diff --git a/src/calibre/constants.py b/src/calibre/constants.py index 138a631b7c..186eb37e34 100644 --- a/src/calibre/constants.py +++ b/src/calibre/constants.py @@ -2,7 +2,7 @@ __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' __docformat__ = 'restructuredtext en' __appname__ = 'calibre' -__version__ = '0.5.7' +__version__ = '0.5.8' __author__ = "Kovid Goyal " ''' Various run time constants. diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index ade60fcc9f..e0e9158f0e 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -282,6 +282,9 @@ from calibre.ebooks.pdb.input import PDBInput from calibre.ebooks.pdf.input import PDFInput from calibre.ebooks.txt.input import TXTInput from calibre.ebooks.lit.input import LITInput +from calibre.ebooks.fb2.input import FB2Input +from calibre.ebooks.odt.input import ODTInput +from calibre.ebooks.rtf.input import RTFInput from calibre.ebooks.html.input import HTMLInput from calibre.ebooks.oeb.output import OEBOutput from calibre.ebooks.txt.output import TXTOutput @@ -289,7 +292,8 @@ from calibre.ebooks.pdf.output import PDFOutput from calibre.customize.profiles import input_profiles, output_profiles plugins = [HTML2ZIP, EPUBInput, MOBIInput, PDBInput, PDFInput, HTMLInput, - TXTInput, OEBOutput, TXTOutput, PDFOutput, LITInput] + TXTInput, OEBOutput, TXTOutput, PDFOutput, LITInput, + FB2Input, ODTInput, RTFInput] plugins += [x for x in list(locals().values()) if isinstance(x, type) and \ x.__name__.endswith('MetadataReader')] plugins += [x for x in list(locals().values()) if isinstance(x, type) and \ diff --git a/src/calibre/ebooks/__init__.py b/src/calibre/ebooks/__init__.py index 26d2394818..79f4f7631e 100644 --- a/src/calibre/ebooks/__init__.py +++ b/src/calibre/ebooks/__init__.py @@ -59,7 +59,10 @@ class HTMLRenderer(object): def render_html(path_to_html, width=590, height=750): from PyQt4.QtWebKit import QWebPage - from PyQt4.Qt import QEventLoop, QPalette, Qt, SIGNAL, QUrl, QSize + from PyQt4.Qt import QEventLoop, QPalette, Qt, SIGNAL, QUrl, QSize, \ + QApplication + if QApplication.instance() is None: + QApplication([]) path_to_html = os.path.abspath(path_to_html) with CurrentDir(os.path.dirname(path_to_html)): page = QWebPage() diff --git a/src/calibre/ebooks/conversion/cli.py b/src/calibre/ebooks/conversion/cli.py index b7336ab30a..a30549cbc3 100644 --- a/src/calibre/ebooks/conversion/cli.py +++ b/src/calibre/ebooks/conversion/cli.py @@ -116,6 +116,25 @@ def add_pipeline_options(parser, plumber): 'font_size_mapping', 'line_height', 'linearize_tables', + 'extra_css', + ] + ), + + 'STRUCTURE DETECTION' : ( + _('Control auto-detection of document structure.'), + [ + 'dont_split_on_page_breaks', 'chapter', 'chapter_mark', + ] + ), + + 'TABLE OF CONTENTS' : ( + _('Control the automatic generation of a Table of Contents. By ' + 'default, if the source file has a Table of Contents, it will ' + 'be used in preference to the automatically generated one.'), + [ + 'level1_toc', 'level2_toc', 'level3_toc', + 'toc_threshold', 'max_toc_links', 'no_chapters_in_toc', + 'use_auto_toc', 'toc_filter', ] ), @@ -130,7 +149,8 @@ def add_pipeline_options(parser, plumber): } - group_order = ['', 'LOOK AND FEEL', 'METADATA', 'DEBUG'] + group_order = ['', 'LOOK AND FEEL', 'STRUCTURE DETECTION', + 'TABLE OF CONTENTS', 'METADATA', 'DEBUG'] for group in group_order: desc, options = groups[group] @@ -163,6 +183,10 @@ def main(args=sys.argv): add_pipeline_options(parser, plumber) opts = parser.parse_args(args)[0] + y = lambda q : os.path.abspath(os.path.expanduser(q)) + for x in ('read_metadata_from_opf', 'cover'): + if getattr(opts, x, None) is not None: + setattr(opts, x, y(getattr(opts, x))) recommendations = [(n.dest, getattr(opts, n.dest), OptionRecommendation.HIGH) \ for n in parser.options_iter() diff --git a/src/calibre/ebooks/conversion/plumber.py b/src/calibre/ebooks/conversion/plumber.py index 1edeed8d9c..22c11303ad 100644 --- a/src/calibre/ebooks/conversion/plumber.py +++ b/src/calibre/ebooks/conversion/plumber.py @@ -3,13 +3,21 @@ __license__ = 'GPL 3' __copyright__ = '2009, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import os +import os, re from calibre.customize.conversion import OptionRecommendation from calibre.customize.ui import input_profiles, output_profiles, \ plugin_for_input_format, plugin_for_output_format from calibre.ebooks.conversion.preprocess import HTMLPreProcessor from calibre.ptempfile import PersistentTemporaryDirectory +from calibre import extract, walk + +def supported_input_formats(): + from calibre.customize.ui import available_input_formats + fmts = available_input_formats() + for x in ('zip', 'rar', 'oebzip'): + fmts.add(x) + return fmts class OptionValues(object): pass @@ -121,6 +129,105 @@ OptionRecommendation(name='dont_split_on_page_breaks', ) ), +OptionRecommendation(name='level1_toc', + recommended_value=None, level=OptionRecommendation.LOW, + help=_('XPath expression that specifies all tags that ' + 'should be added to the Table of Contents at level one. If ' + 'this is specified, it takes precedence over other forms ' + 'of auto-detection.' + ) + ), + +OptionRecommendation(name='level2_toc', + recommended_value=None, level=OptionRecommendation.LOW, + help=_('XPath expression that specifies all tags that should be ' + 'added to the Table of Contents at level two. Each entry is added ' + 'under the previous level one entry.' + ) + ), + +OptionRecommendation(name='level3_toc', + recommended_value=None, level=OptionRecommendation.LOW, + help=_('XPath expression that specifies all tags that should be ' + 'added to the Table of Contents at level three. Each entry ' + 'is added under the previous level two entry.' + ) + ), + +OptionRecommendation(name='use_auto_toc', + recommended_value=False, level=OptionRecommendation.LOW, + help=_('Normally, if the source file already has a Table of ' + 'Contents, it is used in preference to the auto-generated one. ' + 'With this option, the auto-generated one is always used.' + ) + ), + +OptionRecommendation(name='no_chapters_in_toc', + recommended_value=False, level=OptionRecommendation.LOW, + help=_("Don't add auto-detected chapters to the Table of " + 'Contents.' + ) + ), + +OptionRecommendation(name='toc_threshold', + recommended_value=6, level=OptionRecommendation.LOW, + help=_( + 'If fewer than this number of chapters is detected, then links ' + 'are added to the Table of Contents. Default: %default') + ), + +OptionRecommendation(name='max_toc_links', + recommended_value=50, level=OptionRecommendation.LOW, + help=_('Maximum number of links to insert into the TOC. Set to 0 ' + 'to disable. Default is: %default. Links are only added to the ' + 'TOC if less than the threshold number of chapters were detected.' + ) + ), + +OptionRecommendation(name='toc_filter', + recommended_value=None, level=OptionRecommendation.LOW, + help=_('Remove entries from the Table of Contents whose titles ' + 'match the specified regular expression. Matching entries and all ' + 'their children are removed.' + ) + ), + + +OptionRecommendation(name='chapter', + recommended_value="//*[((name()='h1' or name()='h2') and " + "re:test(., 'chapter|book|section|part', 'i')) or @class " + "= 'chapter']", level=OptionRecommendation.LOW, + help=_('An XPath expression to detect chapter titles. The default ' + 'is to consider

or

tags that contain the words ' + '"chapter","book","section" or "part" as chapter titles as ' + 'well as any tags that have class="chapter". The expression ' + 'used must evaluate to a list of elements. To disable chapter ' + 'detection, use the expression "/". See the XPath Tutorial ' + 'in the calibre User Manual for further help on using this ' + 'feature.' + ) + ), + +OptionRecommendation(name='chapter_mark', + recommended_value='pagebreak', level=OptionRecommendation.LOW, + choices=['pagebreak', 'rule', 'both', 'none'], + help=_('Specify how to mark detected chapters. A value of ' + '"pagebreak" will insert page breaks before chapters. ' + 'A value of "rule" will insert a line before chapters. ' + 'A value of "none" will disable chapter marking and a ' + 'value of "both" will use both page breaks and lines ' + 'to mark chapters.') + ), + +OptionRecommendation(name='extra_css', + recommended_value=None, level=OptionRecommendation.LOW, + help=_('Either the path to a CSS stylesheet or raw CSS. ' + 'This CSS will be appended to the style rules from ' + 'the source file, so it can be used to override those ' + 'rules.') + ), + + OptionRecommendation(name='read_metadata_from_opf', recommended_value=None, level=OptionRecommendation.LOW, @@ -130,6 +237,7 @@ OptionRecommendation(name='read_metadata_from_opf', 'file.') ), + OptionRecommendation(name='title', recommended_value=None, level=OptionRecommendation.LOW, help=_('Set the title.')), @@ -187,11 +295,14 @@ OptionRecommendation(name='language', help=_('Set the language.')), ] - input_fmt = os.path.splitext(self.input)[1] if not input_fmt: raise ValueError('Input file must have an extension') input_fmt = input_fmt[1:].lower() + if input_fmt in ('zip', 'rar', 'oebzip'): + self.log('Processing archive...') + tdir = PersistentTemporaryDirectory('_plumber') + self.input, input_fmt = self.unarchive(self.input, tdir) if os.path.exists(self.output) and os.path.isdir(self.output): output_fmt = 'oeb' @@ -201,7 +312,7 @@ OptionRecommendation(name='language', output_fmt = '.oeb' output_fmt = output_fmt[1:].lower() - self.input_plugin = plugin_for_input_format(input_fmt) + self.input_plugin = plugin_for_input_format(input_fmt) self.output_plugin = plugin_for_output_format(output_fmt) if self.input_plugin is None: @@ -224,6 +335,43 @@ OptionRecommendation(name='language', # plugins. self.merge_plugin_recommendations() + @classmethod + def unarchive(self, path, tdir): + extract(path, tdir) + files = list(walk(tdir)) + from calibre.customize.ui import available_input_formats + fmts = available_input_formats() + for x in ('htm', 'html', 'xhtm', 'xhtml'): fmts.remove(x) + + for ext in fmts: + for f in files: + if f.lower().endswith('.'+ext): + if ext in ['txt', 'rtf'] and os.stat(f).st_size < 2048: + continue + return f, ext + return self.find_html_index(files) + + @classmethod + def find_html_index(self, files): + ''' + Given a list of files, find the most likely root HTML file in the + list. + ''' + html_pat = re.compile(r'\.(x){0,1}htm(l){0,1}$', re.IGNORECASE) + html_files = [f for f in files if html_pat.search(f) is not None] + if not html_files: + raise ValueError(_('Could not find an ebook inside the archive')) + html_files = [(f, os.stat(f).st_size) for f in html_files] + html_files.sort(cmp = lambda x, y: cmp(x[1], y[1])) + html_files = [f[0] for f in html_files] + for q in ('toc', 'index'): + for f in html_files: + if os.path.splitext(os.path.basename(f))[0].lower() == q: + return f, os.path.splitext(f)[1].lower()[1:] + return html_files[-1], os.path.splitext(html_files[-1])[1].lower()[1:] + + + def get_option_by_name(self, name): for group in (self.input_options, self.pipeline_options, self.output_options): @@ -237,6 +385,7 @@ OptionRecommendation(name='language', rec = self.get_option_by_name(name) if rec is not None and rec.level <= level: rec.recommended_value = val + rec.level = level def merge_ui_recommendations(self, recommendations): ''' @@ -248,6 +397,7 @@ OptionRecommendation(name='language', rec = self.get_option_by_name(name) if rec is not None and rec.level <= level and rec.level < rec.HIGH: rec.recommended_value = val + rec.level = level def read_user_metadata(self): ''' @@ -332,6 +482,9 @@ OptionRecommendation(name='language', self.opts.source = self.opts.input_profile self.opts.dest = self.opts.output_profile + from calibre.ebooks.oeb.transforms.structure import DetectStructure + DetectStructure()(self.oeb, self.opts) + from calibre.ebooks.oeb.transforms.flatcss import CSSFlattener fbase = self.opts.base_font_size if fbase == 0: @@ -342,6 +495,9 @@ OptionRecommendation(name='language', else: fkey = map(float, fkey.split(',')) + if self.opts.extra_css and os.path.exists(self.opts.extra_css): + self.opts.extra_css = open(self.opts.extra_css, 'rb').read() + flattener = CSSFlattener(fbase=fbase, fkey=fkey, lineh=self.opts.line_height, untable=self.opts.linearize_tables) @@ -364,6 +520,8 @@ OptionRecommendation(name='language', trimmer = ManifestTrimmer() trimmer(self.oeb, self.opts) + self.oeb.toc.rationalize_play_orders() + self.log.info('Creating %s...'%self.output_plugin.name) self.output_plugin.convert(self.oeb, self.output, self.input_plugin, self.opts, self.log) @@ -384,4 +542,3 @@ def create_oebbook(log, path_or_stream, opts, reader=None): reader()(oeb, path_or_stream) return oeb - diff --git a/src/calibre/ebooks/epub/from_any.py b/src/calibre/ebooks/epub/from_any.py index b3e5281525..2f3f81124f 100644 --- a/src/calibre/ebooks/epub/from_any.py +++ b/src/calibre/ebooks/epub/from_any.py @@ -15,130 +15,17 @@ from calibre.ebooks import DRMError from calibre.ebooks.epub import config as common_config from calibre.ebooks.epub.from_html import convert as html2epub, find_html_index from calibre.ptempfile import TemporaryDirectory -from calibre.ebooks.metadata import MetaInformation -from calibre.ebooks.metadata.opf2 import OPFCreator, OPF from calibre.utils.zipfile import ZipFile from calibre.customize.ui import run_plugins_on_preprocess -def lit2opf(path, tdir, opts): - from calibre.ebooks.lit.reader import LitReader - print 'Exploding LIT file:', path - reader = LitReader(path) - reader.extract_content(tdir, False) - opf = None - for opf in walk(tdir): - if opf.lower().endswith('.opf'): - break - if not opf.endswith('.opf'): - opf = None - if opf is not None: # Check for url-quoted filenames - _opf = OPF(opf, os.path.dirname(opf)) - replacements = [] - for item in _opf.itermanifest(): - href = item.get('href', '') - path = os.path.join(os.path.dirname(opf), *(href.split('/'))) - if not os.path.exists(path) and os.path.exists(path.replace('&', '%26')): - npath = path - path = path.replace('&', '%26') - replacements.append((path, npath)) - if replacements: - print 'Fixing quoted filenames...' - for path, npath in replacements: - if os.path.exists(path): - os.rename(path, npath) - for f in walk(tdir): - with open(f, 'r+b') as f: - raw = f.read() - for path, npath in replacements: - raw = raw.replace(os.path.basename(path), os.path.basename(npath)) - f.seek(0) - f.truncate() - f.write(raw) - return opf -def mobi2opf(path, tdir, opts): - from calibre.ebooks.mobi.reader import MobiReader - print 'Exploding MOBI file:', path.encode('utf-8') if isinstance(path, unicode) else path - reader = MobiReader(path) - reader.extract_content(tdir) - files = list(walk(tdir)) - opts.encoding = 'utf-8' - for f in files: - if f.lower().endswith('.opf'): - return f - html_pat = re.compile(r'\.(x){0,1}htm(l){0,1}', re.IGNORECASE) - hf = [f for f in files if html_pat.match(os.path.splitext(f)[1]) is not None] - mi = MetaInformation(os.path.splitext(os.path.basename(path))[0], [_('Unknown')]) - opf = OPFCreator(tdir, mi) - opf.create_manifest([(hf[0], None)]) - opf.create_spine([hf[0]]) - ans = os.path.join(tdir, 'metadata.opf') - opf.render(open(ans, 'wb')) - return ans - -def fb22opf(path, tdir, opts): - from calibre.ebooks.lrf.fb2.convert_from import to_html - print 'Converting FB2 to HTML...' - return to_html(path, tdir) - -def rtf2opf(path, tdir, opts): - from calibre.ebooks.lrf.rtf.convert_from import generate_html - generate_html(path, tdir) - return os.path.join(tdir, 'metadata.opf') - -def txt2opf(path, tdir, opts): - from calibre.ebooks.lrf.txt.convert_from import generate_html - generate_html(path, opts.encoding, tdir) - return os.path.join(tdir, 'metadata.opf') - -def pdf2opf(path, tdir, opts): - from calibre.ebooks.lrf.pdf.convert_from import generate_html - generate_html(path, tdir) - opts.dont_split_on_page_breaks = True - return os.path.join(tdir, 'metadata.opf') - -def epub2opf(path, tdir, opts): - zf = ZipFile(path) - zf.extractall(tdir) - opts.chapter_mark = 'none' - encfile = os.path.join(tdir, 'META-INF', 'encryption.xml') - opf = None - for f in walk(tdir): - if f.lower().endswith('.opf'): - opf = f - break - if opf and os.path.exists(encfile): - if not process_encryption(encfile, opf): - raise DRMError(os.path.basename(path)) - - if opf is None: - raise ValueError('%s is not a valid EPUB file'%path) - return opf - -def odt2epub(path, tdir, opts): - from calibre.ebooks.odt.to_oeb import Extract - opts.encoding = 'utf-8' - return Extract()(path, tdir) - -MAP = { - 'lit' : lit2opf, - 'mobi' : mobi2opf, - 'prc' : mobi2opf, - 'azw' : mobi2opf, - 'fb2' : fb22opf, - 'rtf' : rtf2opf, - 'txt' : txt2opf, - 'pdf' : pdf2opf, - 'epub' : epub2opf, - 'odt' : odt2epub, - } -SOURCE_FORMATS = ['lit', 'mobi', 'prc', 'azw', 'fb2', 'odt', 'rtf', +SOURCE_FORMATS = ['lit', 'mobi', 'prc', 'azw', 'fb2', 'odt', 'rtf', 'txt', 'pdf', 'rar', 'zip', 'oebzip', 'htm', 'html', 'epub'] def unarchive(path, tdir): extract(path, tdir) files = list(walk(tdir)) - + for ext in ['opf'] + list(MAP.keys()): for f in files: if f.lower().endswith('.'+ext): @@ -147,32 +34,32 @@ def unarchive(path, tdir): return f, ext return find_html_index(files) -def any2epub(opts, path, notification=None, create_epub=True, +def any2epub(opts, path, notification=None, create_epub=True, oeb_cover=False, extract_to=None): path = run_plugins_on_preprocess(path) ext = os.path.splitext(path)[1] if not ext: raise ValueError('Unknown file type: '+path) ext = ext.lower()[1:] - + if opts.output is None: opts.output = os.path.splitext(os.path.basename(path))[0]+'.epub' - + with nested(TemporaryDirectory('_any2epub1'), TemporaryDirectory('_any2epub2')) as (tdir1, tdir2): if ext in ['rar', 'zip', 'oebzip']: path, ext = unarchive(path, tdir1) print 'Found %s file in archive'%(ext.upper()) - + if ext in MAP.keys(): path = MAP[ext](path, tdir2, opts) ext = 'opf' - - + + if re.match(r'((x){0,1}htm(l){0,1})|opf', ext) is None: raise ValueError('Conversion from %s is not supported'%ext.upper()) - + print 'Creating EPUB file...' - html2epub(path, opts, notification=notification, + html2epub(path, opts, notification=notification, create_epub=create_epub, oeb_cover=oeb_cover, extract_to=extract_to) diff --git a/src/calibre/ebooks/epub/input.py b/src/calibre/ebooks/epub/input.py index 5c8a5c9d89..919416ffdc 100644 --- a/src/calibre/ebooks/epub/input.py +++ b/src/calibre/ebooks/epub/input.py @@ -11,12 +11,12 @@ from lxml import etree from calibre.customize.conversion import InputFormatPlugin class EPUBInput(InputFormatPlugin): - + name = 'EPUB Input' author = 'Kovid Goyal' description = 'Convert EPUB files (.epub) to HTML' file_types = set(['epub']) - + @classmethod def decrypt_font(cls, key, path): raw = open(path, 'rb').read() @@ -26,7 +26,7 @@ class EPUBInput(InputFormatPlugin): with open(path, 'wb') as f: f.write(decrypt) f.write(raw[1024:]) - + @classmethod def process_ecryption(cls, encfile, opf, log): key = None @@ -51,25 +51,75 @@ class EPUBInput(InputFormatPlugin): traceback.print_exc() return False + @classmethod + def rationalize_cover(self, opf): + guide_cover, guide_elem = None, None + for guide_elem in opf.iterguide(): + if guide_elem.get('type', '').lower() == 'cover': + guide_cover = guide_elem.get('href', '') + break + if not guide_cover: + return + spine = list(opf.iterspine()) + if not spine: + return + idref = spine[0].get('idref', '') + manifest = list(opf.itermanifest()) + if not manifest: + return + if manifest[0].get('id', False) != idref: + return + spine[0].getparent().remove(spine[0]) + guide_elem.set('href', 'calibre_raster_cover.jpg') + for elem in list(opf.iterguide()): + if elem.get('type', '').lower() == 'titlepage': + elem.getparent().remove(elem) + from calibre.ebooks.oeb.base import OPF + t = etree.SubElement(guide_elem.getparent(), OPF('reference')) + t.set('type', 'titlepage') + t.set('href', guide_cover) + t.set('title', 'Title Page') + from calibre.ebooks import render_html + open('calibre_raster_cover.jpg', 'wb').write( + render_html(guide_cover).data) + + def convert(self, stream, options, file_ext, log, accelerators): from calibre.utils.zipfile import ZipFile from calibre import walk from calibre.ebooks import DRMError + from calibre.ebooks.metadata.opf2 import OPF zf = ZipFile(stream) zf.extractall(os.getcwd()) encfile = os.path.abspath(os.path.join('META-INF', 'encryption.xml')) opf = None - for f in walk('.'): + for f in walk(u'.'): if f.lower().endswith('.opf'): - opf = f + opf = os.path.abspath(f) break path = getattr(stream, 'name', 'stream') - + if opf is None: raise ValueError('%s is not a valid EPUB file'%path) - + if os.path.exists(encfile): if not self.process_encryption(encfile, opf, log): raise DRMError(os.path.basename(path)) - return os.path.join(os.getcwd(), opf) + opf = os.path.relpath(opf, os.getcwdu()) + parts = os.path.split(opf) + opf = OPF(opf, os.path.dirname(os.path.abspath(opf))) + + if len(parts) > 1: + delta = '/'.join(parts[:-1])+'/' + for elem in opf.itermanifest(): + elem.set('href', delta+elem.get('href')) + for elem in opf.iterguide(): + elem.set('href', delta+elem.get('href')) + + self.rationalize_cover(opf) + + with open('content.opf', 'wb') as nopf: + nopf.write(opf.render()) + + return os.path.abspath('content.opf') diff --git a/src/calibre/ebooks/lrf/fb2/__init__.py b/src/calibre/ebooks/fb2/__init__.py similarity index 100% rename from src/calibre/ebooks/lrf/fb2/__init__.py rename to src/calibre/ebooks/fb2/__init__.py diff --git a/src/calibre/ebooks/lrf/fb2/fb2.xsl b/src/calibre/ebooks/fb2/fb2.xsl similarity index 100% rename from src/calibre/ebooks/lrf/fb2/fb2.xsl rename to src/calibre/ebooks/fb2/fb2.xsl diff --git a/src/calibre/ebooks/fb2/input.py b/src/calibre/ebooks/fb2/input.py new file mode 100644 index 0000000000..d96758a4bd --- /dev/null +++ b/src/calibre/ebooks/fb2/input.py @@ -0,0 +1,74 @@ +from __future__ import with_statement +__license__ = 'GPL v3' +__copyright__ = '2008, Anatoly Shipitsin ' +""" +Convert .fb2 files to .lrf +""" +import os +from base64 import b64decode +from lxml import etree + +from calibre.customize.conversion import InputFormatPlugin, OptionRecommendation +from calibre import guess_type + +FB2NS = 'http://www.gribuser.ru/xml/fictionbook/2.0' + +class FB2Input(InputFormatPlugin): + + name = 'FB2 Input' + author = 'Anatoly Shipitsin' + description = 'Convert FB2 files to HTML' + file_types = set(['fb2']) + + recommendations = set([ + ('level1_toc', '//h:h1', OptionRecommendation.MED), + ('level2_toc', '//h:h2', OptionRecommendation.MED), + ('level3_toc', '//h:h3', OptionRecommendation.MED), + ]) + + def convert(self, stream, options, file_ext, log, + accelerators): + from calibre.resources import fb2_xsl + from calibre.ebooks.metadata.opf2 import OPFCreator + from calibre.ebooks.metadata.meta import get_metadata + from calibre.ebooks.oeb.base import XLINK_NS + NAMESPACES = {'f':FB2NS, 'l':XLINK_NS} + + log.debug('Parsing XML...') + parser = etree.XMLParser(recover=True, no_network=True) + doc = etree.parse(stream, parser) + self.extract_embedded_content(doc) + log.debug('Converting XML to HTML...') + styledoc = etree.fromstring(fb2_xsl) + + transform = etree.XSLT(styledoc) + result = transform(doc) + open('index.xhtml', 'wb').write(transform.tostring(result)) + stream.seek(0) + mi = get_metadata(stream, 'fb2') + if not mi.title: + mi.title = _('Unknown') + if not mi.authors: + mi.authors = [_('Unknown')] + opf = OPFCreator(os.getcwdu(), mi) + entries = [(f, guess_type(f)[0]) for f in os.listdir('.')] + opf.create_manifest(entries) + opf.create_spine(['index.xhtml']) + + for img in doc.xpath('//f:coverpage/f:image', namespaces=NAMESPACES): + href = img.get('{%s}href'%XLINK_NS, img.get('href', None)) + if href is not None: + if href.startswith('#'): + href = href[1:] + opf.guide.set_cover(os.path.abspath(href)) + + opf.render(open('metadata.opf', 'wb')) + return os.path.join(os.getcwd(), 'metadata.opf') + + def extract_embedded_content(self, doc): + for elem in doc.xpath('./*'): + if 'binary' in elem.tag and elem.attrib.has_key('id'): + fname = elem.attrib['id'] + data = b64decode(elem.text.strip()) + open(fname, 'wb').write(data) + diff --git a/src/calibre/ebooks/lrf/fb2/convert_from.py b/src/calibre/ebooks/lrf/fb2/convert_from.py deleted file mode 100644 index 24562e708c..0000000000 --- a/src/calibre/ebooks/lrf/fb2/convert_from.py +++ /dev/null @@ -1,125 +0,0 @@ -from __future__ import with_statement -__license__ = 'GPL v3' -__copyright__ = '2008, Anatoly Shipitsin ' -""" -Convert .fb2 files to .lrf -""" -import os, sys, shutil, logging -from base64 import b64decode -from lxml import etree - -from calibre.ebooks.lrf import option_parser as lrf_option_parser -from calibre.ebooks.metadata.meta import get_metadata -from calibre.ebooks.lrf.html.convert_from import process_file as html_process_file -from calibre import setup_cli_handlers -from calibre.resources import fb2_xsl -from calibre.ptempfile import PersistentTemporaryDirectory -from calibre.ebooks.metadata.opf import OPFCreator -from calibre.ebooks.metadata import MetaInformation - - -def option_parser(): - parser = lrf_option_parser( -_('''%prog [options] mybook.fb2 - - -%prog converts mybook.fb2 to mybook.lrf''')) - parser.add_option('--debug-html-generation', action='store_true', default=False, - dest='debug_html_generation', help=_('Print generated HTML to stdout and quit.')) - parser.add_option('--keep-intermediate-files', action='store_true', default=False, - help=_('Keep generated HTML files after completing conversion to LRF.')) - return parser - -def extract_embedded_content(doc): - for elem in doc.xpath('./*'): - if 'binary' in elem.tag and elem.attrib.has_key('id'): - fname = elem.attrib['id'] - data = b64decode(elem.text.strip()) - open(fname, 'wb').write(data) - -def to_html(fb2file, tdir): - fb2file = os.path.abspath(fb2file) - cwd = os.getcwd() - try: - os.chdir(tdir) - print 'Parsing XML...' - parser = etree.XMLParser(recover=True, no_network=True) - doc = etree.parse(fb2file, parser) - extract_embedded_content(doc) - print 'Converting XML to HTML...' - styledoc = etree.fromstring(fb2_xsl) - - transform = etree.XSLT(styledoc) - result = transform(doc) - open('index.html', 'wb').write(transform.tostring(result)) - try: - mi = get_metadata(open(fb2file, 'rb'), 'fb2') - except: - mi = MetaInformation(None, None) - if not mi.title: - mi.title = os.path.splitext(os.path.basename(fb2file))[0] - if not mi.authors: - mi.authors = [_('Unknown')] - opf = OPFCreator(tdir, mi) - opf.create_manifest([('index.html', None)]) - opf.create_spine(['index.html']) - opf.render(open('metadata.opf', 'wb')) - return os.path.join(tdir, 'metadata.opf') - finally: - os.chdir(cwd) - - -def generate_html(fb2file, encoding, logger): - tdir = PersistentTemporaryDirectory('_fb22lrf') - to_html(fb2file, tdir) - return os.path.join(tdir, 'index.html') - -def process_file(path, options, logger=None): - if logger is None: - level = logging.DEBUG if options.verbose else logging.INFO - logger = logging.getLogger('fb22lrf') - setup_cli_handlers(logger, level) - fb2 = os.path.abspath(os.path.expanduser(path)) - f = open(fb2, 'rb') - mi = get_metadata(f, 'fb2') - f.close() - htmlfile = generate_html(fb2, options.encoding, logger) - tdir = os.path.dirname(htmlfile) - cwd = os.getcwdu() - try: - if not options.output: - ext = '.lrs' if options.lrs else '.lrf' - options.output = os.path.abspath(os.path.basename(os.path.splitext(path)[0]) + ext) - options.output = os.path.abspath(os.path.expanduser(options.output)) - if not mi.title: - mi.title = os.path.splitext(os.path.basename(fb2))[0] - if (not options.title or options.title == _('Unknown')): - options.title = mi.title - if (not options.author or options.author == _('Unknown')) and mi.authors: - options.author = mi.authors.pop() - if (not options.category or options.category == _('Unknown')) and mi.category: - options.category = mi.category - if (not options.freetext or options.freetext == _('Unknown')) and mi.comments: - options.freetext = mi.comments - os.chdir(tdir) - html_process_file(htmlfile, options, logger) - finally: - os.chdir(cwd) - if getattr(options, 'keep_intermediate_files', False): - logger.debug('Intermediate files in '+ tdir) - else: - shutil.rmtree(tdir) - -def main(args=sys.argv, logger=None): - parser = option_parser() - options, args = parser.parse_args(args) - if len(args) != 2: - parser.print_help() - print - print 'No fb2 file specified' - return 1 - process_file(args[1], options, logger) - return 0 - -if __name__ == '__main__': - sys.exit(main()) diff --git a/src/calibre/ebooks/lrf/rtf/convert_from.py b/src/calibre/ebooks/lrf/rtf/convert_from.py deleted file mode 100644 index e4dd153d2a..0000000000 --- a/src/calibre/ebooks/lrf/rtf/convert_from.py +++ /dev/null @@ -1,190 +0,0 @@ -__license__ = 'GPL v3' -__copyright__ = '2008, Kovid Goyal ' -import os, sys, shutil, logging, glob - -from lxml import etree - -from calibre.ebooks.lrf import option_parser as lrf_option_parser -from calibre.ebooks.metadata.meta import get_metadata -from calibre.ebooks.lrf.html.convert_from import process_file as html_process_file -from calibre import setup_cli_handlers -from calibre.libwand import convert, WandException -from calibre.ebooks.BeautifulSoup import BeautifulStoneSoup -from calibre.ebooks.lrf.rtf.xsl import xhtml -from calibre.ebooks.rtf2xml.ParseRtf import RtfInvalidCodeException -from calibre.ptempfile import PersistentTemporaryDirectory -from calibre.ebooks.metadata import MetaInformation -from calibre.ebooks.metadata.opf import OPFCreator - -def option_parser(): - parser = lrf_option_parser( -_('''%prog [options] mybook.rtf - - -%prog converts mybook.rtf to mybook.lrf''') - ) - parser.add_option('--keep-intermediate-files', action='store_true', default=False) - return parser - -def convert_images(html, logger): - wmfs = glob.glob('*.wmf') + glob.glob('*.WMF') - for wmf in wmfs: - target = os.path.join(os.path.dirname(wmf), os.path.splitext(os.path.basename(wmf))[0]+'.jpg') - try: - convert(wmf, target) - html = html.replace(os.path.basename(wmf), os.path.basename(target)) - except WandException, err: - logger.warning(u'Unable to convert image %s with error: %s'%(wmf, unicode(err))) - continue - return html - -def process_file(path, options, logger=None): - if logger is None: - level = logging.DEBUG if options.verbose else logging.INFO - logger = logging.getLogger('rtf2lrf') - setup_cli_handlers(logger, level) - rtf = os.path.abspath(os.path.expanduser(path)) - f = open(rtf, 'rb') - mi = get_metadata(f, 'rtf') - f.close() - tdir = PersistentTemporaryDirectory('_rtf2lrf') - html = generate_html(rtf, tdir) - cwd = os.getcwdu() - try: - if not options.output: - ext = '.lrs' if options.lrs else '.lrf' - options.output = os.path.abspath(os.path.basename(os.path.splitext(path)[0]) + ext) - options.output = os.path.abspath(os.path.expanduser(options.output)) - if not mi.title: - mi.title = os.path.splitext(os.path.basename(rtf))[0] - if (not options.title or options.title == 'Unknown'): - options.title = mi.title - if (not options.author or options.author == 'Unknown') and mi.author: - options.author = mi.author - if (not options.category or options.category == 'Unknown') and mi.category: - options.category = mi.category - if (not options.freetext or options.freetext == 'Unknown') and mi.comments: - options.freetext = mi.comments - os.chdir(tdir) - html_process_file(html, options, logger) - finally: - os.chdir(cwd) - if hasattr(options, 'keep_intermediate_files') and options.keep_intermediate_files: - logger.debug('Intermediate files in '+ tdir) - else: - shutil.rmtree(tdir) - -def main(args=sys.argv, logger=None): - parser = option_parser() - options, args = parser.parse_args(args) - if len(args) != 2: - parser.print_help() - print - print 'No rtf file specified' - return 1 - process_file(args[1], options, logger) - return 0 - - -def generate_xml(rtfpath, tdir): - from calibre.ebooks.rtf2xml.ParseRtf import ParseRtf - ofile = os.path.join(tdir, 'index.xml') - cwd = os.getcwdu() - os.chdir(tdir) - rtfpath = os.path.abspath(rtfpath) - try: - parser = ParseRtf( - in_file = rtfpath, - out_file = ofile, - # Convert symbol fonts to unicode equivelents. Default - # is 1 - convert_symbol = 1, - - # Convert Zapf fonts to unicode equivelents. Default - # is 1. - convert_zapf = 1, - - # Convert Wingding fonts to unicode equivelents. - # Default is 1. - convert_wingdings = 1, - - # Convert RTF caps to real caps. - # Default is 1. - convert_caps = 1, - - # Indent resulting XML. - # Default is 0 (no indent). - indent = 1, - - # Form lists from RTF. Default is 1. - form_lists = 1, - - # Convert headings to sections. Default is 0. - headings_to_sections = 1, - - # Group paragraphs with the same style name. Default is 1. - group_styles = 1, - - # Group borders. Default is 1. - group_borders = 1, - - # Write or do not write paragraphs. Default is 0. - empty_paragraphs = 0, - ) - parser.parse_rtf() - finally: - os.chdir(cwd) - return ofile - - -def generate_html(rtfpath, tdir): - print 'Converting RTF to XML...' - rtfpath = os.path.abspath(rtfpath) - try: - xml = generate_xml(rtfpath, tdir) - except RtfInvalidCodeException: - raise Exception(_('This RTF file has a feature calibre does not support. Convert it to HTML and then convert it.')) - tdir = os.path.dirname(xml) - cwd = os.getcwdu() - os.chdir(tdir) - try: - print 'Parsing XML...' - parser = etree.XMLParser(recover=True, no_network=True) - try: - doc = etree.parse(xml, parser) - except: - raise - print 'Parsing failed. Trying to clean up XML...' - soup = BeautifulStoneSoup(open(xml, 'rb').read()) - doc = etree.fromstring(str(soup)) - print 'Converting XML to HTML...' - styledoc = etree.fromstring(xhtml) - - transform = etree.XSLT(styledoc) - result = transform(doc) - tdir = os.path.dirname(xml) - html = os.path.join(tdir, 'index.html') - f = open(html, 'wb') - res = transform.tostring(result) - res = res[:100].replace('xmlns:html', 'xmlns') + res[100:] - f.write(res) - f.close() - try: - mi = get_metadata(open(rtfpath, 'rb'), 'rtf') - except: - mi = MetaInformation(None, None) - if not mi.title: - mi.title = os.path.splitext(os.path.basename(rtfpath))[0] - if not mi.authors: - mi.authors = [_('Unknown')] - opf = OPFCreator(tdir, mi) - opf.create_manifest([('index.html', None)]) - opf.create_spine(['index.html']) - opf.render(open('metadata.opf', 'wb')) - finally: - os.chdir(cwd) - return html - -if __name__ == '__main__': - sys.exit(main()) - \ No newline at end of file diff --git a/src/calibre/ebooks/metadata/library_thing.py b/src/calibre/ebooks/metadata/library_thing.py index ef41d5e937..0f46c72c75 100644 --- a/src/calibre/ebooks/metadata/library_thing.py +++ b/src/calibre/ebooks/metadata/library_thing.py @@ -4,13 +4,15 @@ __copyright__ = '2008, Kovid Goyal ' Fetch cover from LibraryThing.com based on ISBN number. ''' -import sys, socket, os, re, mechanize +import sys, socket, os, re from calibre import browser as _browser from calibre.utils.config import OptionParser -from calibre.ebooks.BeautifulSoup import BeautifulSoup +from calibre.ebooks.BeautifulSoup import BeautifulSoup browser = None +OPENLIBRARY = 'http://covers.openlibrary.org/b/isbn/%s-L.jpg?default=false' + class LibraryThingError(Exception): pass @@ -30,15 +32,21 @@ def login(username, password, force=True): browser['formusername'] = username browser['formpassword'] = password browser.submit() - -def cover_from_isbn(isbn, timeout=5.): + +def cover_from_isbn(isbn, timeout=5., username=None, password=None): global browser if browser is None: browser = _browser() _timeout = socket.getdefaulttimeout() socket.setdefaulttimeout(timeout) - src = None + src = None + try: + return browser.open(OPENLIBRARY%isbn).read(), 'jpg' + except: + pass # Cover not found + if username and password: + login(username, password, force=False) try: src = browser.open('http://www.librarything.com/isbn/'+isbn).read().decode('utf-8', 'replace') except Exception, err: @@ -55,7 +63,7 @@ def cover_from_isbn(isbn, timeout=5.): url = url.find('img') if url is None: raise LibraryThingError(_('LibraryThing.com server error. Try again later.')) - url = re.sub(r'_SX\d+', '', url['src']) + url = re.sub(r'_S[XY]\d+', '', url['src']) cover_data = browser.open(url).read() return cover_data, url.rpartition('.')[-1] finally: @@ -68,9 +76,9 @@ _(''' Fetch a cover image for the book identified by ISBN from LibraryThing.com ''')) - parser.add_option('-u', '--username', default=None, + parser.add_option('-u', '--username', default=None, help='Username for LibraryThing.com') - parser.add_option('-p', '--password', default=None, + parser.add_option('-p', '--password', default=None, help='Password for LibraryThing.com') return parser @@ -81,13 +89,8 @@ def main(args=sys.argv): parser.print_help() return 1 isbn = args[1] - if opts.username and opts.password: - try: - login(opts.username, opts.password) - except mechanize.FormNotFoundError: - raise LibraryThingError(_('LibraryThing.com server error. Try again later.')) - - cover_data, ext = cover_from_isbn(isbn) + cover_data, ext = cover_from_isbn(isbn, username=opts.username, + password=opts.password) if not ext: ext = 'jpg' oname = os.path.abspath(isbn+'.'+ext) @@ -96,4 +99,4 @@ def main(args=sys.argv): return 0 if __name__ == '__main__': - sys.exit(main()) \ No newline at end of file + sys.exit(main()) diff --git a/src/calibre/ebooks/odt/input.py b/src/calibre/ebooks/odt/input.py new file mode 100644 index 0000000000..7d6498ab81 --- /dev/null +++ b/src/calibre/ebooks/odt/input.py @@ -0,0 +1,67 @@ +from __future__ import with_statement +__license__ = 'GPL v3' +__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' +__docformat__ = 'restructuredtext en' + +''' +Convert an ODT file into a Open Ebook +''' +import os +from odf.odf2xhtml import ODF2XHTML + +from calibre import CurrentDir, walk +from calibre.customize.conversion import InputFormatPlugin + +class Extract(ODF2XHTML): + + def extract_pictures(self, zf): + if not os.path.exists('Pictures'): + os.makedirs('Pictures') + for name in zf.namelist(): + if name.startswith('Pictures'): + data = zf.read(name) + with open(name, 'wb') as f: + f.write(data) + + def __call__(self, stream, odir): + from calibre.utils.zipfile import ZipFile + from calibre.ebooks.metadata.meta import get_metadata + from calibre.ebooks.metadata.opf2 import OPFCreator + + + if not os.path.exists(odir): + os.makedirs(odir) + with CurrentDir(odir): + print 'Extracting ODT file...' + html = self.odf2xhtml(stream) + with open('index.xhtml', 'wb') as f: + f.write(html.encode('utf-8')) + zf = ZipFile(stream, 'r') + self.extract_pictures(zf) + stream.seek(0) + mi = get_metadata(stream, 'odt') + if not mi.title: + mi.title = _('Unknown') + if not mi.authors: + mi.authors = [_('Unknown')] + opf = OPFCreator(os.path.abspath(os.getcwdu()), mi) + opf.create_manifest([(os.path.abspath(f), None) for f in walk(os.getcwd())]) + opf.create_spine([os.path.abspath('index.xhtml')]) + with open('metadata.opf', 'wb') as f: + opf.render(f) + return os.path.abspath('metadata.opf') + + +class ODTInput(InputFormatPlugin): + + name = 'ODT Input' + author = 'Kovid Goyal' + description = 'Convert ODT (OpenOffice) files to HTML' + file_types = set(['odt']) + + + def convert(self, stream, options, file_ext, log, + accelerators): + return Extract()(stream, '.') + + diff --git a/src/calibre/ebooks/odt/to_oeb.py b/src/calibre/ebooks/odt/to_oeb.py deleted file mode 100644 index 7cb354884e..0000000000 --- a/src/calibre/ebooks/odt/to_oeb.py +++ /dev/null @@ -1,72 +0,0 @@ -from __future__ import with_statement -__license__ = 'GPL v3' -__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' -__docformat__ = 'restructuredtext en' - -''' -Convert an ODT file into a Open Ebook -''' -import os, sys -from odf.odf2xhtml import ODF2XHTML - -from calibre import CurrentDir, walk -from calibre.utils.zipfile import ZipFile -from calibre.utils.config import OptionParser -from calibre.ebooks.metadata.odt import get_metadata -from calibre.ebooks.metadata.opf2 import OPFCreator - -class Extract(ODF2XHTML): - - def extract_pictures(self, zf): - if not os.path.exists('Pictures'): - os.makedirs('Pictures') - for name in zf.namelist(): - if name.startswith('Pictures'): - data = zf.read(name) - with open(name, 'wb') as f: - f.write(data) - - def __call__(self, path, odir): - if not os.path.exists(odir): - os.makedirs(odir) - path = os.path.abspath(path) - with CurrentDir(odir): - print 'Extracting ODT file...' - html = self.odf2xhtml(path) - with open('index.html', 'wb') as f: - f.write(html.encode('utf-8')) - with open(path, 'rb') as f: - zf = ZipFile(f, 'r') - self.extract_pictures(zf) - f.seek(0) - mi = get_metadata(f) - if not mi.title: - mi.title = os.path.splitext(os.path.basename(path)) - if not mi.authors: - mi.authors = [_('Unknown')] - opf = OPFCreator(os.path.abspath(os.getcwdu()), mi) - opf.create_manifest([(os.path.abspath(f), None) for f in walk(os.getcwd())]) - opf.create_spine([os.path.abspath('index.html')]) - with open('metadata.opf', 'wb') as f: - opf.render(f) - return os.path.abspath('metadata.opf') - -def option_parser(): - parser = OptionParser('%prog [options] file.odt') - parser.add_option('-o', '--output-dir', default='.', - help=_('The output directory. Defaults to the current directory.')) - return parser - -def main(args=sys.argv): - parser = option_parser() - opts, args = parser.parse_args(args) - if len(args) < 2: - parser.print_help() - print 'No ODT file specified' - return 1 - Extract()(args[1], os.path.abspath(opts.output_dir)) - print 'Extracted to', os.path.abspath(opts.output_dir) - return 0 - -if __name__ == '__main__': - sys.exit(main()) \ No newline at end of file diff --git a/src/calibre/ebooks/oeb/base.py b/src/calibre/ebooks/oeb/base.py index dda36a7500..a36ad8f676 100644 --- a/src/calibre/ebooks/oeb/base.py +++ b/src/calibre/ebooks/oeb/base.py @@ -41,10 +41,12 @@ NCX_NS = 'http://www.daisy.org/z3986/2005/ncx/' SVG_NS = 'http://www.w3.org/2000/svg' XLINK_NS = 'http://www.w3.org/1999/xlink' CALIBRE_NS = 'http://calibre.kovidgoyal.net/2009/metadata' +RE_NS = 'http://exslt.org/regular-expressions' + XPNSMAP = {'h' : XHTML_NS, 'o1' : OPF1_NS, 'o2' : OPF2_NS, 'd09': DC09_NS, 'd10': DC10_NS, 'd11': DC11_NS, 'xsi': XSI_NS, 'dt' : DCTERMS_NS, 'ncx': NCX_NS, - 'svg': SVG_NS, 'xl' : XLINK_NS} + 'svg': SVG_NS, 'xl' : XLINK_NS, 're': RE_NS} OPF1_NSMAP = {'dc': DC11_NS, 'oebpackage': OPF1_NS} OPF2_NSMAP = {'opf': OPF2_NS, 'dc': DC11_NS, 'dcterms': DCTERMS_NS, 'xsi': XSI_NS, 'calibre': CALIBRE_NS} @@ -1024,7 +1026,7 @@ class Manifest(object): media_type = XHTML_MIME elif media_type in OEB_STYLES: media_type = CSS_MIME - attrib = {'id': item.id, 'href': item.href, + attrib = {'id': item.id, 'href': urlunquote(item.href), 'media-type': media_type} if item.fallback: attrib['fallback'] = item.fallback @@ -1236,7 +1238,7 @@ class Guide(object): def to_opf2(self, parent=None): elem = element(parent, OPF('guide')) for ref in self.refs.values(): - attrib = {'type': ref.type, 'href': ref.href} + attrib = {'type': ref.type, 'href': urlunquote(ref.href)} if ref.title: attrib['title'] = ref.title element(elem, OPF('reference'), attrib=attrib) @@ -1256,19 +1258,34 @@ class TOC(object): :attr:`klass`: Optional semantic class referenced by this node. :attr:`id`: Option unique identifier for this node. """ - def __init__(self, title=None, href=None, klass=None, id=None): + def __init__(self, title=None, href=None, klass=None, id=None, + play_order=None): self.title = title self.href = urlnormalize(href) if href else href self.klass = klass self.id = id self.nodes = [] + self.play_order = 0 + if play_order is None: + play_order = self.next_play_order() + self.play_order = play_order - def add(self, title, href, klass=None, id=None): + def add(self, title, href, klass=None, id=None, play_order=0): """Create and return a new sub-node of this node.""" - node = TOC(title, href, klass, id) + node = TOC(title, href, klass, id, play_order) self.nodes.append(node) return node + def remove(self, node): + for child in self.nodes: + if child is node: + self.nodes.remove(child) + return True + else: + if child.remove(node): + return True + return False + def iter(self): """Iterate over this node and all descendants in depth-first order.""" yield self @@ -1276,6 +1293,18 @@ class TOC(object): for node in child.iter(): yield node + def count(self): + return len(list(self.iter())) - 1 + + def next_play_order(self): + return max([x.play_order for x in self.iter()])+1 + + def has_href(self, href): + for x in self.iter(): + if x.href == href: + return True + return False + def iterdescendants(self): """Iterate over all descendant nodes in depth-first order.""" for child in self.nodes: @@ -1309,6 +1338,10 @@ class TOC(object): except ValueError: return 1 + def __str__(self): + return 'TOC: %s --> %s'%(self.title, self.href) + + def to_opf1(self, tour): for node in self.nodes: element(tour, 'site', attrib={ @@ -1319,7 +1352,7 @@ class TOC(object): def to_ncx(self, parent): for node in self.nodes: id = node.id or unicode(uuid.uuid4()) - attrib = {'id': id, 'playOrder': '0'} + attrib = {'id': id, 'playOrder': str(node.play_order)} if node.klass: attrib['class'] = node.klass point = element(parent, NCX('navPoint'), attrib=attrib) @@ -1329,6 +1362,34 @@ class TOC(object): node.to_ncx(point) return parent + def rationalize_play_orders(self): + ''' + Ensure that all nodes with the same play_order have the same href and + with different play_orders have different hrefs. + ''' + def po_node(n): + for x in self.iter(): + if x is n: + return + if x.play_order == n.play_order: + return x + + def href_node(n): + for x in self.iter(): + if x is n: + return + if x.href == n.href: + return x + + for x in self.iter(): + y = po_node(x) + if y is not None: + if x.href != y.href: + x.play_order = getattr(href_node(x), 'play_order', + self.next_play_order()) + y = href_node(x) + if y is not None: + x.play_order = y.play_order class PageList(object): """Collection of named "pages" to mapped positions within an OEB data model diff --git a/src/calibre/ebooks/oeb/iterator.py b/src/calibre/ebooks/oeb/iterator.py index 81e1f89029..ab3e90083d 100644 --- a/src/calibre/ebooks/oeb/iterator.py +++ b/src/calibre/ebooks/oeb/iterator.py @@ -118,6 +118,7 @@ class EbookIterator(object): print 'Loaded embedded font:', repr(family) def __enter__(self): + self.delete_on_exit = [] self._tdir = TemporaryDirectory('_ebook_iter') self.base = self._tdir.__enter__() from calibre.ebooks.conversion.plumber import Plumber @@ -137,9 +138,11 @@ class EbookIterator(object): cover = self.opf.cover if self.ebook_ext in ('lit', 'mobi', 'prc', 'opf') and cover: - cfile = os.path.join(os.path.dirname(self.spine[0]), 'calibre_ei_cover.html') + cfile = os.path.join(os.path.dirname(self.spine[0]), + 'calibre_iterator_cover.html') open(cfile, 'wb').write(TITLEPAGE%cover) self.spine[0:0] = [SpineItem(cfile)] + self.delete_on_exit.append(cfile) if self.opf.path_to_html_toc is not None and \ self.opf.path_to_html_toc not in self.spine: @@ -221,3 +224,6 @@ class EbookIterator(object): def __exit__(self, *args): self._tdir.__exit__(*args) + for x in self.delete_on_exit: + if os.path.exists(x): + os.remove(x) diff --git a/src/calibre/ebooks/oeb/reader.py b/src/calibre/ebooks/oeb/reader.py index 6f0ff44bc9..02b3b92b01 100644 --- a/src/calibre/ebooks/oeb/reader.py +++ b/src/calibre/ebooks/oeb/reader.py @@ -343,7 +343,8 @@ class OEBReader(object): continue id = child.get('id') klass = child.get('class') - node = toc.add(title, href, id=id, klass=klass) + po = int(child.get('playOrder', self.oeb.toc.next_play_order())) + node = toc.add(title, href, id=id, klass=klass, play_order=po) self._toc_from_navpoint(item, node, child) def _toc_from_ncx(self, item): diff --git a/src/calibre/ebooks/oeb/stylizer.py b/src/calibre/ebooks/oeb/stylizer.py index 8bc82883e3..34abea32f5 100644 --- a/src/calibre/ebooks/oeb/stylizer.py +++ b/src/calibre/ebooks/oeb/stylizer.py @@ -88,7 +88,7 @@ FONT_SIZE_NAMES = set(['xx-small', 'x-small', 'small', 'medium', 'large', class CSSSelector(etree.XPath): MIN_SPACE_RE = re.compile(r' *([>~+]) *') LOCAL_NAME_RE = re.compile(r"(?' +__docformat__ = 'restructuredtext en' + +import re + +from lxml import etree +from urlparse import urlparse + +from calibre.ebooks.oeb.base import XPNSMAP, TOC +XPath = lambda x: etree.XPath(x, namespaces=XPNSMAP) + +class DetectStructure(object): + + def __call__(self, oeb, opts): + self.log = oeb.log + self.oeb = oeb + self.opts = opts + self.log('Detecting structure...') + + self.detect_chapters() + if self.oeb.auto_generated_toc or opts.use_auto_toc: + orig_toc = self.oeb.toc + self.oeb.toc = TOC() + self.create_level_based_toc() + if self.oeb.toc.count() < 1: + if not opts.no_chapters_in_toc and self.detected_chapters: + self.create_toc_from_chapters() + if self.oeb.toc.count() < opts.toc_threshold: + self.create_toc_from_links() + if self.oeb.toc.count() < 2 and orig_toc.count() > 2: + self.oeb.toc = orig_toc + else: + self.oeb.auto_generated_toc = True + self.log('Auto generated TOC with %d entries.' % + self.oeb.toc.count()) + + if opts.toc_filter is not None: + regexp = re.compile(opts.toc_filter) + for node in self.oeb.toc.iter(): + if not node.title or regexp.search(node.title) is not None: + self.oeb.toc.remove(node) + + + def detect_chapters(self): + self.detected_chapters = [] + if self.opts.chapter: + chapter_xpath = XPath(self.opts.chapter) + for item in self.oeb.spine: + for x in chapter_xpath(item.data): + self.detected_chapters.append((item, x)) + + chapter_mark = self.opts.chapter_mark + page_break_before = 'display: block; page-break-before: always' + page_break_after = 'display: block; page-break-after: always' + for item, elem in self.detected_chapters: + text = u' '.join([t.strip() for t in elem.xpath('descendant::text()')]) + self.log('\tDetected chapter:', text[:50]) + if chapter_mark == 'none': + continue + elif chapter_mark == 'rule': + mark = etree.Element('hr') + elif chapter_mark == 'pagebreak': + mark = etree.Element('div', style=page_break_after) + else: # chapter_mark == 'both': + mark = etree.Element('hr', style=page_break_before) + elem.addprevious(mark) + + def create_level_based_toc(self): + if self.opts.level1_toc is None: + return + for item in self.oeb.spine: + self.add_leveled_toc_items(item) + + def create_toc_from_chapters(self): + counter = self.oeb.toc.next_play_order() + for item, elem in self.detected_chapters: + text, href = self.elem_to_link(item, elem, counter) + self.oeb.toc.add(text, href, play_order=counter) + counter += 1 + + def create_toc_from_links(self): + for item in self.oeb.spine: + for a in item.data.xpath('//h:a[@href]'): + href = a.get('href') + purl = urlparse(href) + if not purl[0] or purl[0] == 'file': + href, frag = purl.path, purl.fragment + href = item.abshref(href) + if frag: + href = '#'.join((href, frag)) + if not self.oeb.toc.has_href(href): + text = u' '.join([t.strip() for t in \ + a.xpath('descendant::text()')]) + text = text[:100].strip() + if not self.oeb.toc.has_text(text): + self.oeb.toc.add(text, href, + play_order=self.oeb.toc.next_play_order()) + + + def elem_to_link(self, item, elem, counter): + text = u' '.join([t.strip() for t in elem.xpath('descendant::text()')]) + text = text[:100].strip() + id = elem.get('id', 'calibre_toc_%d'%counter) + elem.set('id', id) + href = '#'.join((item.href, id)) + return text, href + + + def add_leveled_toc_items(self, item): + level1 = XPath(self.opts.level1_toc)(item.data) + level1_order = [] + + counter = 1 + if level1: + added = {} + for elem in level1: + text, _href = self.elem_to_link(item, elem, counter) + counter += 1 + if text: + node = self.oeb.toc.add(text, _href, + play_order=self.oeb.toc.next_play_order()) + level1_order.append(node) + added[elem] = node + #node.add(_('Top'), _href) + if self.opts.level2_toc is not None: + added2 = {} + level2 = list(XPath(self.opts.level2_toc)(item.data)) + for elem in level2: + level1 = None + for item in item.data.iterdescendants(): + if item in added.keys(): + level1 = added[item] + elif item == elem and level1 is not None: + text, _href = self.elem_to_link(item, elem, counter) + counter += 1 + if text: + added2[elem] = level1.add(text, _href, + play_order=self.oeb.toc.next_play_order()) + if self.opts.level3_toc is not None: + level3 = list(XPath(self.opts.level3_toc)(item.data)) + for elem in level3: + level2 = None + for item in item.data.iterdescendants(): + if item in added2.keys(): + level2 = added2[item] + elif item == elem and level2 is not None: + text, _href = \ + self.elem_to_link(item, elem, counter) + counter += 1 + if text: + level2.add(text, _href, + play_order=self.oeb.toc.next_play_order()) + + diff --git a/src/calibre/ebooks/lrf/rtf/__init__.py b/src/calibre/ebooks/rtf/__init__.py similarity index 100% rename from src/calibre/ebooks/lrf/rtf/__init__.py rename to src/calibre/ebooks/rtf/__init__.py diff --git a/src/calibre/ebooks/rtf/input.py b/src/calibre/ebooks/rtf/input.py new file mode 100644 index 0000000000..764d47ff41 --- /dev/null +++ b/src/calibre/ebooks/rtf/input.py @@ -0,0 +1,101 @@ +from __future__ import with_statement +__license__ = 'GPL v3' +__copyright__ = '2008, Kovid Goyal ' + +import os + +from lxml import etree + +from calibre.customize.conversion import InputFormatPlugin + +class RTFInput(InputFormatPlugin): + + name = 'RTF Input' + author = 'Kovid Goyal' + description = 'Convert RTF files to HTML' + file_types = set(['rtf']) + + def generate_xml(self, stream): + from calibre.ebooks.rtf2xml.ParseRtf import ParseRtf + ofile = 'out.xml' + parser = ParseRtf( + in_file = stream, + out_file = ofile, + # Convert symbol fonts to unicode equivelents. Default + # is 1 + convert_symbol = 1, + + # Convert Zapf fonts to unicode equivelents. Default + # is 1. + convert_zapf = 1, + + # Convert Wingding fonts to unicode equivelents. + # Default is 1. + convert_wingdings = 1, + + # Convert RTF caps to real caps. + # Default is 1. + convert_caps = 1, + + # Indent resulting XML. + # Default is 0 (no indent). + indent = 1, + + # Form lists from RTF. Default is 1. + form_lists = 1, + + # Convert headings to sections. Default is 0. + headings_to_sections = 1, + + # Group paragraphs with the same style name. Default is 1. + group_styles = 1, + + # Group borders. Default is 1. + group_borders = 1, + + # Write or do not write paragraphs. Default is 0. + empty_paragraphs = 0, + ) + parser.parse_rtf() + ans = open('out.xml').read() + os.remove('out.xml') + return ans + + def convert(self, stream, options, file_ext, log, + accelerators): + from calibre.ebooks.rtf.xsl import xhtml + from calibre.ebooks.metadata.meta import get_metadata + from calibre.ebooks.metadata.opf import OPFCreator + from calibre.ebooks.rtf2xml.ParseRtf import RtfInvalidCodeException + self.log = log + self.log('Converting RTF to XML...') + try: + xml = self.generate_xml(stream) + except RtfInvalidCodeException: + raise ValueError(_('This RTF file has a feature calibre does not ' + 'support. Convert it to HTML first and then try it.')) + self.log('Parsing XML...') + parser = etree.XMLParser(recover=True, no_network=True) + doc = etree.fromstring(xml, parser=parser) + self.log('Converting XML to HTML...') + styledoc = etree.fromstring(xhtml) + + transform = etree.XSLT(styledoc) + result = transform(doc) + html = 'index.xhtml' + with open(html, 'wb') as f: + res = transform.tostring(result) + res = res[:100].replace('xmlns:html', 'xmlns') + res[100:] + f.write(res) + stream.seek(0) + mi = get_metadata(stream, 'rtf') + if not mi.title: + mi.title = _('Unknown') + if not mi.authors: + mi.authors = [_('Unknown')] + opf = OPFCreator(os.getcwd(), mi) + opf.create_manifest([('index.xhtml', None)]) + opf.create_spine(['index.xhtml']) + opf.render(open('metadata.opf', 'wb')) + return os.path.abspath('metadata.opf') + diff --git a/src/calibre/ebooks/lrf/rtf/xsl.py b/src/calibre/ebooks/rtf/xsl.py similarity index 100% rename from src/calibre/ebooks/lrf/rtf/xsl.py rename to src/calibre/ebooks/rtf/xsl.py diff --git a/src/calibre/ebooks/rtf2xml/ParseRtf.py b/src/calibre/ebooks/rtf2xml/ParseRtf.py index 5b008df615..cba0f900db 100755 --- a/src/calibre/ebooks/rtf2xml/ParseRtf.py +++ b/src/calibre/ebooks/rtf2xml/ParseRtf.py @@ -149,9 +149,10 @@ class ParseRtf: self.__group_borders = group_borders self.__empty_paragraphs = empty_paragraphs self.__no_dtd = no_dtd - + def __check_file(self, the_file, type): """Check to see if files exist""" + if hasattr(the_file, 'read'): return if the_file == None: if type == "file_to_parse": message = "You must provide a file for the script to work" @@ -545,13 +546,12 @@ class ParseRtf: def __make_temp_file(self,file): """Make a temporary file to parse""" write_file="rtf_write_file" - read_obj = open(file,'r') + read_obj = file if hasattr(file, 'read') else open(file,'r') write_obj = open(write_file, 'w') line = "dummy" while line: line = read_obj.read(1000) write_obj.write(line ) - read_obj.close() write_obj.close() return write_file """ diff --git a/src/calibre/ebooks/rtf2xml/pict.py b/src/calibre/ebooks/rtf2xml/pict.py index b1931b8c2e..6c88dd54e4 100755 --- a/src/calibre/ebooks/rtf2xml/pict.py +++ b/src/calibre/ebooks/rtf2xml/pict.py @@ -58,10 +58,12 @@ class Pict: return line[18:] def __make_dir(self): """ Make a dirctory to put the image data in""" - base_name = os.path.basename(self.__orig_file) + base_name = os.path.basename(getattr(self.__orig_file, 'name', + self.__orig_file)) base_name = os.path.splitext(base_name)[0] if self.__out_file: - dir_name = os.path.dirname(self.__out_file) + dir_name = os.path.dirname(getattr(self.__out_file, 'name', + self.__out_file)) else: dir_name = os.path.dirname(self.__orig_file) # self.__output_to_file_func() diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py index 4a74c87097..e3e2080cc0 100644 --- a/src/calibre/gui2/dialogs/metadata_single.py +++ b/src/calibre/gui2/dialogs/metadata_single.py @@ -16,16 +16,14 @@ from calibre.gui2 import qstring_to_unicode, error_dialog, file_icon_provider, \ from calibre.gui2.dialogs.metadata_single_ui import Ui_MetadataSingleDialog from calibre.gui2.dialogs.fetch_metadata import FetchMetadata from calibre.gui2.dialogs.tag_editor import TagEditor -from calibre.gui2.dialogs.password import PasswordDialog from calibre.gui2.widgets import ProgressIndicator from calibre.ebooks import BOOK_EXTENSIONS from calibre.ebooks.metadata import authors_to_sort_string, string_to_authors, authors_to_string -from calibre.ebooks.metadata.library_thing import login, cover_from_isbn +from calibre.ebooks.metadata.library_thing import cover_from_isbn from calibre import islinux from calibre.ebooks.metadata.meta import get_metadata from calibre.utils.config import prefs from calibre.customize.ui import run_plugins_on_import -from calibre.gui2 import config as gui_conf class CoverFetcher(QThread): @@ -60,9 +58,8 @@ class CoverFetcher(QThread): return self.isbn = results[0] - if self.username and self.password: - login(self.username, self.password, force=False) - self.cover_data = cover_from_isbn(self.isbn, timeout=self.timeout)[0] + self.cover_data = cover_from_isbn(self.isbn, timeout=self.timeout, + username=self.username, password=self.password)[0] except Exception, e: self.exception = e self.traceback = traceback.format_exc() @@ -290,7 +287,6 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): self.series_index.setValue(self.db.series_index(row)) QObject.connect(self.series, SIGNAL('currentIndexChanged(int)'), self.enable_series_index) QObject.connect(self.series, SIGNAL('editTextChanged(QString)'), self.enable_series_index) - QObject.connect(self.password_button, SIGNAL('clicked()'), self.change_password) self.show() height_of_rest = self.frameGeometry().height() - self.cover.height() @@ -363,30 +359,12 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): tag_string = ', '.join(d.tags) self.tags.setText(tag_string) - def lt_password_dialog(self): - return PasswordDialog(self, 'LibraryThing account', - _('

Enter your username and password for ' - 'LibraryThing.com. This is optional. It will ' - 'make fetching of covers faster and more reliable.
If ' - 'you do not have an account, you can ' - 'register for ' - 'free.

')) - - def change_password(self): - d = self.lt_password_dialog() - d.exec_() - def fetch_cover(self): isbn = unicode(self.isbn.text()).strip() - d = self.lt_password_dialog() - if not gui_conf['asked_library_thing_password'] and \ - (not d.username() or not d.password()): - d.exec_() - gui_conf['asked_library_thing_password'] = True self.fetch_cover_button.setEnabled(False) self.setCursor(Qt.WaitCursor) title, author = map(unicode, (self.title.text(), self.authors.text())) - self.cover_fetcher = CoverFetcher(d.username(), d.password(), isbn, + self.cover_fetcher = CoverFetcher(None, None, isbn, self.timeout, title, author) self.cover_fetcher.start() self._hangcheck = QTimer(self) diff --git a/src/calibre/gui2/dialogs/metadata_single.ui b/src/calibre/gui2/dialogs/metadata_single.ui index 6e96c8d741..2c4ab859a3 100644 --- a/src/calibre/gui2/dialogs/metadata_single.ui +++ b/src/calibre/gui2/dialogs/metadata_single.ui @@ -589,17 +589,7 @@ - Fetch &cover image from server - - - - - - - Change the username and/or password for your account at LibraryThing.com - - - Change &password + Download &cover @@ -655,7 +645,6 @@ comments fetch_metadata_button fetch_cover_button - password_button formats add_format_button remove_format_button diff --git a/src/calibre/linux.py b/src/calibre/linux.py index ee51370b61..2d13ea2730 100644 --- a/src/calibre/linux.py +++ b/src/calibre/linux.py @@ -27,10 +27,6 @@ entry_points = { 'lrs2lrf = calibre.ebooks.lrf.lrs.convert_from:main', 'isbndb = calibre.ebooks.metadata.isbndb:main', 'librarything = calibre.ebooks.metadata.library_thing:main', - 'comic2lrf = calibre.ebooks.lrf.comic.convert_from:main', - 'comic2epub = calibre.ebooks.epub.from_comic:main', - 'comic2mobi = calibre.ebooks.mobi.from_comic:main', - 'comic2pdf = calibre.ebooks.pdf.from_comic:main', 'calibre-debug = calibre.debug:main', 'calibredb = calibre.library.cli:main', 'calibre-fontconfig = calibre.utils.fontconfig:main', @@ -151,8 +147,6 @@ def setup_completion(fatal_errors): from calibre.ebooks.lrf.pdf.reflow import option_parser as pdfhtmlop from calibre.web.feeds.main import option_parser as feeds2disk from calibre.web.feeds.recipes import titles as feed_titles - from calibre.ebooks.lrf.comic.convert_from import option_parser as comicop - from calibre.ebooks.epub.from_comic import option_parser as comic2epub from calibre.ebooks.metadata.fetch import option_parser as fem_op from calibre.gui2.main import option_parser as guiop from calibre.utils.smtp import option_parser as smtp_op @@ -181,10 +175,6 @@ def setup_completion(fatal_errors): f.write(opts_and_exts('ebook-meta', metaop, list(meta_filetypes()))) f.write(opts_and_exts('lrfviewer', lrfviewerop, ['lrf'])) f.write(opts_and_exts('pdfrelow', pdfhtmlop, ['pdf'])) - f.write(opts_and_exts('comic2lrf', comicop, ['cbz', 'cbr'])) - f.write(opts_and_exts('comic2epub', comic2epub, ['cbz', 'cbr'])) - f.write(opts_and_exts('comic2mobi', comic2epub, ['cbz', 'cbr'])) - f.write(opts_and_exts('comic2pdf', comic2epub, ['cbz', 'cbr'])) f.write(opts_and_words('feeds2disk', feeds2disk, feed_titles)) f.write(opts_and_words('fetch-ebook-metadata', fem_op, [])) f.write(opts_and_words('calibre-smtp', smtp_op, [])) diff --git a/src/calibre/translations/ar.po b/src/calibre/translations/ar.po index cc771f54f0..50d56ea137 100644 --- a/src/calibre/translations/ar.po +++ b/src/calibre/translations/ar.po @@ -7,14 +7,14 @@ msgid "" msgstr "" "Project-Id-Version: calibre\n" "Report-Msgid-Bugs-To: FULL NAME \n" -"POT-Creation-Date: 2009-03-29 04:54+0000\n" +"POT-Creation-Date: 2009-04-16 20:18+0000\n" "PO-Revision-Date: 2009-04-04 16:00+0000\n" "Last-Translator: صقر بن عبدالله \n" "Language-Team: Arabic \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2009-04-08 23:20+0000\n" +"X-Launchpad-Export-Date: 2009-04-19 22:56+0000\n" "X-Generator: Launchpad (build Unknown)\n" #: /home/kovid/work/calibre/src/calibre/customize/__init__.py:41 @@ -43,11 +43,11 @@ msgstr "لا يفعل شيءً" #: /home/kovid/work/calibre/src/calibre/ebooks/lrf/pdf/convert_from.py:82 #: /home/kovid/work/calibre/src/calibre/ebooks/lrf/rtf/convert_from.py:179 #: /home/kovid/work/calibre/src/calibre/ebooks/lrf/txt/convert_from.py:70 -#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/__init__.py:200 -#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/__init__.py:230 -#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/__init__.py:233 -#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/__init__.py:272 -#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/__init__.py:296 +#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/__init__.py:199 +#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/__init__.py:229 +#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/__init__.py:232 +#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/__init__.py:271 +#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/__init__.py:301 #: /home/kovid/work/calibre/src/calibre/ebooks/metadata/meta.py:53 #: /home/kovid/work/calibre/src/calibre/ebooks/metadata/meta.py:55 #: /home/kovid/work/calibre/src/calibre/ebooks/metadata/meta.py:95 @@ -55,27 +55,27 @@ msgstr "لا يفعل شيءً" #: /home/kovid/work/calibre/src/calibre/ebooks/metadata/mobi.py:148 #: /home/kovid/work/calibre/src/calibre/ebooks/metadata/opf.py:334 #: /home/kovid/work/calibre/src/calibre/ebooks/metadata/opf.py:449 -#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/opf2.py:862 +#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/opf2.py:860 #: /home/kovid/work/calibre/src/calibre/ebooks/metadata/pdf.py:12 #: /home/kovid/work/calibre/src/calibre/ebooks/metadata/topaz.py:29 #: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader.py:37 -#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader.py:60 -#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader.py:69 -#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader.py:135 -#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader.py:564 +#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader.py:61 +#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader.py:70 +#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader.py:138 +#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader.py:622 #: /home/kovid/work/calibre/src/calibre/ebooks/odt/to_oeb.py:46 -#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:573 -#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:578 -#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:1154 +#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:576 +#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:581 #: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:1157 +#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:1160 #: /home/kovid/work/calibre/src/calibre/ebooks/pdf/pdftrim.py:53 #: /home/kovid/work/calibre/src/calibre/ebooks/pdf/pdftrim.py:54 -#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:182 -#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:189 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:445 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:454 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:606 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:609 +#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:186 +#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:193 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:453 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:462 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:614 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:617 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf.py:48 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/epub.py:171 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/epub.py:173 @@ -85,25 +85,25 @@ msgstr "لا يفعل شيءً" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:33 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:38 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:39 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:121 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:122 #: /home/kovid/work/calibre/src/calibre/gui2/library.py:364 #: /home/kovid/work/calibre/src/calibre/gui2/library.py:377 #: /home/kovid/work/calibre/src/calibre/gui2/library.py:905 #: /home/kovid/work/calibre/src/calibre/gui2/tools.py:61 #: /home/kovid/work/calibre/src/calibre/gui2/tools.py:123 -#: /home/kovid/work/calibre/src/calibre/library/cli.py:263 +#: /home/kovid/work/calibre/src/calibre/library/cli.py:264 #: /home/kovid/work/calibre/src/calibre/library/database.py:916 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:482 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:494 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:876 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:911 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:1218 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:1220 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:1400 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:1423 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:1474 -#: /home/kovid/work/calibre/src/calibre/library/server.py:327 -#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:51 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:497 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:509 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:894 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:929 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:1236 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:1238 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:1418 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:1441 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:1492 +#: /home/kovid/work/calibre/src/calibre/library/server.py:340 +#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:50 msgid "Unknown" msgstr "مجهول" @@ -165,6 +165,7 @@ msgstr "إقرأ ميتاداتا لكتب في أرشيفات RAR" #: /home/kovid/work/calibre/src/calibre/customize/builtins.py:228 #: /home/kovid/work/calibre/src/calibre/customize/builtins.py:238 #: /home/kovid/work/calibre/src/calibre/customize/builtins.py:248 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:259 msgid "Set metadata in %s files" msgstr "ضبط الميتاداتا في الملفات %s" @@ -184,74 +185,70 @@ msgstr "تخصيص الملحقات المحلية" msgid "Disabled plugins" msgstr "ملحقات معطلة" -#: /home/kovid/work/calibre/src/calibre/customize/ui.py:66 +#: /home/kovid/work/calibre/src/calibre/customize/ui.py:73 msgid "No valid plugin found in " msgstr "لا يجد ملحق صالح " -#: /home/kovid/work/calibre/src/calibre/customize/ui.py:185 +#: /home/kovid/work/calibre/src/calibre/customize/ui.py:192 msgid "Initialization of plugin %s failed with traceback:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/ui.py:262 +#: /home/kovid/work/calibre/src/calibre/customize/ui.py:269 msgid "" " %prog options\n" -" \n" +"\n" " Customize calibre by loading external plugins.\n" " " msgstr "" -" %prog options\n" -" \n" -"تخصيص كاليبر بتحميل ملحقات خارجية.\n" -" " -#: /home/kovid/work/calibre/src/calibre/customize/ui.py:268 +#: /home/kovid/work/calibre/src/calibre/customize/ui.py:275 msgid "Add a plugin by specifying the path to the zip file containing it." msgstr "إضافة ملحق يتخصيص مسار إلى ملف zip الذي يحتويه." -#: /home/kovid/work/calibre/src/calibre/customize/ui.py:270 +#: /home/kovid/work/calibre/src/calibre/customize/ui.py:277 msgid "Remove a custom plugin by name. Has no effect on builtin plugins" msgstr "حذف الملحق المخصص عن طريق اسمه. لا يؤثر على الملحقات المضمنة" -#: /home/kovid/work/calibre/src/calibre/customize/ui.py:272 +#: /home/kovid/work/calibre/src/calibre/customize/ui.py:279 msgid "" "Customize plugin. Specify name of plugin and customization string separated " "by a comma." msgstr "تخصيص الملحق حدد اسم الملحق وسلسلة التخصيص وفرقهما بفاصلة." -#: /home/kovid/work/calibre/src/calibre/customize/ui.py:274 +#: /home/kovid/work/calibre/src/calibre/customize/ui.py:281 msgid "List all installed plugins" msgstr "قائمة كل الملحقات المثبتة" -#: /home/kovid/work/calibre/src/calibre/customize/ui.py:276 +#: /home/kovid/work/calibre/src/calibre/customize/ui.py:283 msgid "Enable the named plugin" msgstr "تمكين الملحق المسمى" -#: /home/kovid/work/calibre/src/calibre/customize/ui.py:278 +#: /home/kovid/work/calibre/src/calibre/customize/ui.py:285 msgid "Disable the named plugin" msgstr "تعطيل الملحق المسمى" -#: /home/kovid/work/calibre/src/calibre/devices/cybookg3/driver.py:42 +#: /home/kovid/work/calibre/src/calibre/devices/cybookg3/driver.py:41 #: /home/kovid/work/calibre/src/calibre/devices/prs505/driver.py:390 -#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:72 +#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:77 msgid "The reader has no storage card connected." msgstr "ليس للقارئ بطاقة تخزين." -#: /home/kovid/work/calibre/src/calibre/devices/cybookg3/driver.py:61 -#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:91 +#: /home/kovid/work/calibre/src/calibre/devices/cybookg3/driver.py:60 +#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:96 msgid "There is insufficient free space on the storage card" msgstr "لا توجد مساحة كافية في بطاقة التخزين" -#: /home/kovid/work/calibre/src/calibre/devices/cybookg3/driver.py:63 -#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:93 +#: /home/kovid/work/calibre/src/calibre/devices/cybookg3/driver.py:62 +#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:98 msgid "There is insufficient free space in main memory" msgstr "لا توجد مساحة كافية في الذاكرة الرئيسية" #: /home/kovid/work/calibre/src/calibre/devices/prs505/driver.py:140 #: /home/kovid/work/calibre/src/calibre/devices/prs505/driver.py:168 #: /home/kovid/work/calibre/src/calibre/devices/prs505/driver.py:196 -#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:195 -#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:231 -#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:258 +#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:205 +#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:242 +#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:269 msgid "Unable to detect the %s disk drive. Try rebooting." msgstr "لم يتمكن من كشف القرص %s. حاول إعادة التشغيل." @@ -499,7 +496,7 @@ msgstr "" msgid "Could not find an ebook inside the archive" msgstr "لم يتمكّن من الحصول على كتاب داخل الأرشيف" -#: /home/kovid/work/calibre/src/calibre/ebooks/epub/from_html.py:233 +#: /home/kovid/work/calibre/src/calibre/ebooks/epub/from_html.py:262 msgid "" "%prog [options] file.html|opf\n" "\n" @@ -507,16 +504,16 @@ msgid "" "file.\n" "If you specify an OPF file instead of an HTML file, the list of links is " "takes from\n" -"the element of the OPF file. \n" +"the element of the OPF file.\n" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/epub/from_html.py:486 +#: /home/kovid/work/calibre/src/calibre/ebooks/epub/from_html.py:519 #: /home/kovid/work/calibre/src/calibre/ebooks/lit/writer.py:758 #: /home/kovid/work/calibre/src/calibre/ebooks/mobi/writer.py:621 msgid "Output written to " msgstr "تم كتابة الخرج في " -#: /home/kovid/work/calibre/src/calibre/ebooks/epub/from_html.py:508 +#: /home/kovid/work/calibre/src/calibre/ebooks/epub/from_html.py:541 #: /home/kovid/work/calibre/src/calibre/ebooks/html.py:1155 msgid "You must specify an input HTML file" msgstr "يجب أن تخصص ملف HTML لتدخيله" @@ -640,7 +637,7 @@ msgid "%prog [options] LITFILE" msgstr "" #: /home/kovid/work/calibre/src/calibre/ebooks/lit/reader.py:895 -#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader.py:588 +#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader.py:657 msgid "Output directory. Defaults to current directory." msgstr "دليل الخرج. الإفتراضي هو الدليل الحالي." @@ -655,7 +652,7 @@ msgid "Useful for debugging." msgstr "" #: /home/kovid/work/calibre/src/calibre/ebooks/lit/reader.py:912 -#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader.py:612 +#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader.py:681 msgid "OEB ebook created in" msgstr "تم إنشاء كتاب OEB في" @@ -698,7 +695,7 @@ msgid "Sort key for the author" msgstr "مفتاح الترتيب للمؤلف" #: /home/kovid/work/calibre/src/calibre/ebooks/lrf/__init__.py:89 -#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/__init__.py:297 +#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/__init__.py:302 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/fetch_metadata.py:58 #: /home/kovid/work/calibre/src/calibre/gui2/library.py:113 msgid "Publisher" @@ -1329,7 +1326,7 @@ msgstr "" "\n" #: /home/kovid/work/calibre/src/calibre/ebooks/lrf/meta.py:587 -#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/__init__.py:43 +#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/__init__.py:42 msgid "Set the book title" msgstr "تحديد عنوان الكتاب" @@ -1346,7 +1343,7 @@ msgid "Set sort key for the author" msgstr "ضبط مفتاح الترتيب للمؤلف" #: /home/kovid/work/calibre/src/calibre/ebooks/lrf/meta.py:595 -#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/__init__.py:47 +#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/__init__.py:46 msgid "The category this book belongs to. E.g.: History" msgstr "تصنيف الكتاب. مثلاً: تاريخ" @@ -1454,15 +1451,15 @@ msgid "" "%prog converts mybook.txt to mybook.lrf" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/__init__.py:45 +#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/__init__.py:44 msgid "Set the authors" msgstr "تحديد المؤلفين" -#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/__init__.py:49 +#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/__init__.py:48 msgid "Set the comment" msgstr "تحديد التعليق" -#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/__init__.py:295 +#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/__init__.py:300 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/book_info.py:69 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/book_info.py:70 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/fetch_metadata.py:55 @@ -1472,7 +1469,7 @@ msgstr "تحديد التعليق" msgid "Title" msgstr "العنوان" -#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/__init__.py:296 +#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/__init__.py:301 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/fetch_metadata.py:56 #: /home/kovid/work/calibre/src/calibre/gui2/library.py:109 #: /home/kovid/work/calibre/src/calibre/gui2/library.py:366 @@ -1480,11 +1477,11 @@ msgstr "العنوان" msgid "Author(s)" msgstr "المؤلف أو المؤلفون" -#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/__init__.py:298 +#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/__init__.py:303 msgid "Producer" msgstr "المنتج" -#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/__init__.py:299 +#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/__init__.py:304 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/book_info.py:71 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/book_info_ui.py:64 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/epub_ui.py:489 @@ -1495,7 +1492,7 @@ msgstr "المنتج" msgid "Comments" msgstr "التعليقات" -#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/__init__.py:301 +#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/__init__.py:312 #: /home/kovid/work/calibre/src/calibre/gui2/library.py:114 #: /home/kovid/work/calibre/src/calibre/gui2/library.py:311 #: /home/kovid/work/calibre/src/calibre/gui2/library.py:915 @@ -1505,7 +1502,7 @@ msgstr "التعليقات" msgid "Tags" msgstr "الوسوم" -#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/__init__.py:303 +#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/__init__.py:314 #: /home/kovid/work/calibre/src/calibre/gui2/library.py:115 #: /home/kovid/work/calibre/src/calibre/gui2/library.py:327 #: /home/kovid/work/calibre/src/calibre/gui2/status.py:59 @@ -1513,11 +1510,11 @@ msgstr "الوسوم" msgid "Series" msgstr "السلسلة" -#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/__init__.py:304 +#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/__init__.py:315 msgid "Language" msgstr "اللغة" -#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/__init__.py:306 +#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/__init__.py:317 #: /home/kovid/work/calibre/src/calibre/gui2/library.py:914 msgid "Timestamp" msgstr "ختم التوقيت" @@ -1551,7 +1548,7 @@ msgid "Usage: imp-meta file.imp" msgstr "الاستخدام: imp-meta file.imp" #: /home/kovid/work/calibre/src/calibre/ebooks/metadata/imp.py:54 -#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/pdf.py:59 +#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/pdf.py:68 #: /home/kovid/work/calibre/src/calibre/ebooks/metadata/rb.py:60 msgid "No filename specified." msgstr "لم يتم تحديد اسم الملف." @@ -1634,11 +1631,11 @@ msgstr "تحديد اللغة" msgid "Set the ISBN" msgstr "تحديد الـISBN" -#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/opf2.py:1025 +#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/opf2.py:1023 msgid "Set the dc:language field" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/pdf.py:58 +#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/pdf.py:67 msgid "Usage: pdf-meta file.pdf" msgstr "الاستخدام: pdf-meta file.pdf" @@ -1650,11 +1647,11 @@ msgstr "الاستخدام: rb-meta file.rb" msgid "Creating Mobipocket file from EPUB..." msgstr "إنشاء ملف Mobipocket من EPUB..." -#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader.py:586 +#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader.py:655 msgid "%prog [options] myebook.mobi" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader.py:610 +#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader.py:679 msgid "Raw MOBI HTML saved in" msgstr "" @@ -1717,74 +1714,74 @@ msgstr "" msgid "The output directory. Defaults to the current directory." msgstr "دليل الخرج. الإفتراضي هو الدليل الحالي." -#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:826 +#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:829 msgid "Cover" msgstr "الغلاف" -#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:827 +#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:830 msgid "Title Page" msgstr "صقحة العنوان" -#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:828 +#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:831 #: /home/kovid/work/calibre/src/calibre/ebooks/oeb/transforms/htmltoc.py:18 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:47 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main_ui.py:160 msgid "Table of Contents" msgstr "المحتويات" -#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:829 +#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:832 msgid "Index" msgstr "الفهرس" -#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:830 +#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:833 msgid "Glossary" msgstr "المسرد" -#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:831 +#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:834 msgid "Acknowledgements" msgstr "شكر وتقدير" -#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:832 +#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:835 msgid "Bibliography" msgstr "ببليوغرافيا" -#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:833 +#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:836 msgid "Colophon" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:834 +#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:837 msgid "Copyright" msgstr "حقوق المؤلف" -#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:835 +#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:838 msgid "Dedication" msgstr "الإهداء" -#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:836 +#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:839 msgid "Epigraph" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:837 +#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:840 msgid "Foreword" msgstr "افتتاحية" -#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:838 +#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:841 msgid "List of Illustrations" msgstr "قائمة الرسوم" -#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:839 +#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:842 msgid "List of Tables" msgstr "قائمة الجداول" -#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:840 +#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:843 msgid "Notes" msgstr "الملاحظات" -#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:841 +#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:844 msgid "Preface" msgstr "افتتاحية" -#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:842 +#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:845 msgid "Main Text" msgstr "النصّ الرئيسي" @@ -1954,7 +1951,7 @@ msgid "Adding books to database..." msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/add.py:183 -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:751 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:753 msgid "Reading metadata..." msgstr "قراءة الميتاداتا..." @@ -1962,174 +1959,175 @@ msgstr "قراءة الميتاداتا..." msgid "Searching in" msgstr "يتم البحث في" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:85 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:93 msgid "Device no longer connected." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:136 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:144 msgid "Get device information" msgstr "احصل على معلومات الجهاز" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:146 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:154 msgid "Get list of books on device" msgstr "احصل على قائمة الكتب على الجهاز" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:155 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:163 msgid "Send metadata to device" msgstr "ارسل الميتاداتا إلى الجهاز" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:164 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:172 msgid "Upload %d books to device" msgstr "رفع %d كتاب إلى الجهاز" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:179 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:187 msgid "Delete books from device" msgstr "حذف كتب من الجهاز" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:194 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:202 msgid "Download books from device" msgstr "تنزيل الكتب من الجهاز" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:204 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:212 msgid "View book on device" msgstr "عرض كتاب على الجهاز" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:211 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:219 msgid "and delete from library" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:232 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:240 msgid "Set default send to device action" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:237 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:244 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:246 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:248 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:245 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:252 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:254 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:256 msgid "Email to" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:262 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:267 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:270 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:275 msgid "Send to main memory" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:264 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:269 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:272 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:277 msgid "Send to storage card" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:272 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:280 msgid "Send specific format to main memory" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:274 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:282 msgid "Send specific format to storage card" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:402 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:410 msgid "No books" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:403 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:411 msgid "selected to send" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:408 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:416 msgid "Choose format to send to device" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:415 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:423 msgid "No device" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:416 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:424 msgid "Cannot send: No device is connected" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:420 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:428 msgid "No card" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:421 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:429 msgid "Cannot send: Device has no storage card" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:452 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:460 msgid "E-book:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:455 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:463 msgid "Attached, you will find the e-book" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:456 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:464 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:109 msgid "by" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:457 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:465 msgid "in the %s format." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:470 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:478 msgid "Sending email to" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:474 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:620 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:482 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:628 msgid "No suitable formats" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:475 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:483 msgid "" "Could not email the following books as no suitable formats were " "found:
    %s
" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:494 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:502 msgid "Failed to email books" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:495 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:503 msgid "Failed to email the following books:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:499 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:507 msgid "Sent by email:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:526 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:534 msgid "News:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:527 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:535 msgid "Attached is the" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:538 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:546 msgid "Sent news to" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:574 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:582 msgid "Sending news to device." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:617 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:625 msgid "Sending books to device." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:621 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:629 msgid "" "Could not upload the following books to the device, as no suitable formats " -"were found:
    %s
" +"were found. Try changing the output format in the upper right corner next to " +"the red heart and re-converting.
    %s
" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:667 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:677 msgid "No space on device" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:668 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:678 msgid "" "

Cannot upload books to device there is no more free space available " msgstr "" @@ -2165,7 +2163,7 @@ msgstr "حوار" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/book_info_ui.py:63 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/choose_format_ui.py:41 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/confirm_delete_ui.py:49 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/password_ui.py:56 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/password_ui.py:57 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/progress_ui.py:49 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/progress_ui.py:50 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/warning_ui.py:53 @@ -2314,114 +2312,114 @@ msgstr "" msgid "new email address" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:442 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:451 msgid "Finish gmail setup" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:443 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:452 msgid "Dont forget to enter your gmail username and password" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:450 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:457 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:459 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:466 msgid "Bad configuration" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:451 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:460 msgid "You must set the From email address" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:458 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:467 msgid "You must set the username and password for the mail server." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:505 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:514 msgid "No valid plugin path" msgstr "مسار الملحق غير صالح" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:506 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:515 msgid "%s is not a valid plugin path" msgstr "%s ليس مسار لملحق صالح" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:509 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:518 msgid "Choose plugin" msgstr "إختيار الملحق" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:520 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:529 msgid "Plugin cannot be disabled" msgstr "لا يمكن تعطيل الملحق" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:521 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:530 msgid "The plugin: %s cannot be disabled" msgstr "الملحق: %s لا يمكن تعطيله" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:531 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:540 msgid "Plugin not customizable" msgstr "لا يمكن تخصيص الملحق" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:532 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:541 msgid "Plugin: %s does not need customization" msgstr "الملحق: %s لا يحتاج التخصيص" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:535 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:544 msgid "Customize %s" msgstr "تخصيص %s" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:545 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:554 msgid "Cannot remove builtin plugin" msgstr "لم يمكن حذف الملحق المضمن" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:546 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:555 msgid " cannot be removed. It is a builtin plugin. Try disabling it instead." msgstr " لا يمكن حذفه. هذا ملحق مضمن في البرنامج. حاول تعطيله بدلاً من حذفه." -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:568 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:577 msgid "Error log:" msgstr "سجل الأخطاء:" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:575 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:584 msgid "Access log:" msgstr "سجل النفاذ:" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:600 -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:461 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:609 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:463 msgid "Failed to start content server" msgstr "فشل في تشغيل خادم المحتوى" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:624 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:633 msgid "Select database location" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:641 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:650 msgid "Invalid size" msgstr "حجم غير صالح" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:642 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:651 msgid "The size %s is invalid. must be of the form widthxheight" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:682 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:687 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:691 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:696 msgid "Invalid database location" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:683 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:692 msgid "Invalid database location " msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:684 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:693 msgid "
Must be a directory." msgstr "
يجب أن يكون دليل." -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:688 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:697 msgid "Invalid database location.
Cannot write to " msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:702 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:711 msgid "Compacting..." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:703 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:712 msgid "Compacting database. This may take a while." msgstr "" @@ -2551,7 +2549,7 @@ msgid "Automatically send downloaded &news to ebook reader" msgstr "إرسال الأخبار& التي تم تنزيلها آلياً إلى قارئ الكتب الإلكترونية" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:564 -msgid "&Delete news from library when it is sent to reader" +msgid "&Delete news from library when it is automatically sent to reader" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:565 @@ -2653,7 +2651,7 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:590 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:607 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/password_ui.py:57 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/password_ui.py:58 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:173 msgid "&Username:" msgstr "&اسم المستخدم:" @@ -2664,7 +2662,7 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:592 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:608 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/password_ui.py:58 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/password_ui.py:59 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:174 msgid "&Password:" msgstr "&كلمة السرّ" @@ -2737,7 +2735,7 @@ msgid "" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:610 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/password_ui.py:59 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/password_ui.py:60 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:175 msgid "&Show password" msgstr "إظهار& كلمة السرّ" @@ -2889,26 +2887,26 @@ msgstr "إختار الغلاف لـ " #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/epub.py:113 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/lrf_single.py:174 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:81 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:107 msgid "Cannot read" msgstr "لا يمكن القراءة" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/epub.py:114 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/lrf_single.py:175 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:82 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:108 msgid "You do not have permission to read the file: " msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/epub.py:122 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/epub.py:129 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/lrf_single.py:183 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:90 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:116 msgid "Error reading file" msgstr "خطأ في قراءة الملف" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/epub.py:123 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/lrf_single.py:184 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:91 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:117 msgid "

There was an error reading from file:
" msgstr "" @@ -2918,7 +2916,7 @@ msgid " is not a valid picture" msgstr " ليست صورة صالحة" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/epub.py:242 -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:972 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:974 msgid "Cannot convert" msgstr "لا يمكن تحويله" @@ -3336,7 +3334,7 @@ msgid "Convert %s to LRF" msgstr "تحول %s إلى LRF" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/lrf_single.py:109 -#: /home/kovid/work/calibre/src/calibre/gui2/tools.py:360 +#: /home/kovid/work/calibre/src/calibre/gui2/tools.py:366 msgid "Set conversion defaults" msgstr "" @@ -3616,75 +3614,76 @@ msgstr "حذف الت&هيئة:" msgid "A&utomatically set author sort" msgstr "ضبط& ترتيب المؤلف آلياً" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:139 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:140 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:165 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:166 msgid "No format selected" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:150 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:176 msgid "Could not read metadata" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:151 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:177 msgid "Could not read metadata from %s format" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:159 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:165 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:185 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:191 msgid "Could not read cover" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:160 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:186 msgid "Could not read cover from %s format" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:166 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:192 msgid "The cover in the %s format is invalid" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:348 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:374 msgid "" -"

Enter your username and password for LibraryThing.com.
If you " -"do not have one, you can register " -"for free!.

" +"

Enter your username and password for LibraryThing.com. This is " +"optional. It will make fetching of covers faster and more " +"reliable.
If you do not have an account, you can register for free.

" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:370 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:401 msgid "Downloading cover..." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:373 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:384 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:390 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:413 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:418 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:424 msgid "Cannot fetch cover" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:374 -msgid "You must specify the ISBN identifier for this book." -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:385 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:391 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:414 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:425 msgid "Could not fetch cover.
" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:386 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:415 msgid "The download timed out." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:397 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:419 +msgid "Could not find cover for this book. Try specifying the ISBN first." +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:431 msgid "Bad cover" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:398 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:432 msgid "The cover is not a valid picture" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:433 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:467 msgid "Cannot fetch metadata" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:434 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:468 msgid "You must specify at least one of ISBN, Title, Authors or Publisher" msgstr "" @@ -3746,7 +3745,7 @@ msgstr "" msgid "Change &password" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/password_ui.py:55 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/password_ui.py:56 msgid "Password needed" msgstr "" @@ -3758,96 +3757,96 @@ msgstr "" msgid "You" msgstr "أنت" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:123 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:138 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:124 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:139 msgid "Custom" msgstr "مخصّص" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:125 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:136 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:222 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:126 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:137 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:223 msgid "Scheduled" msgstr "تم جدولته" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:234 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:235 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:216 msgid "Search" msgstr "بحث" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:310 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:311 msgid "%d recipes" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:311 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:312 msgid "Monday" msgstr "الأثنين" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:311 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:312 msgid "Tuesday" msgstr "الثلاثاء" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:311 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:312 msgid "Wednesday" msgstr "الأربعاء" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:311 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:312 msgid "day" msgstr "اليوم" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:312 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:313 msgid "Friday" msgstr "الجمعة" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:312 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:313 msgid "Saturday" msgstr "السبت" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:312 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:313 msgid "Sunday" msgstr "الأحد" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:312 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:313 msgid "Thursday" msgstr "الخميس" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:345 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:346 msgid "Must set account information" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:346 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:347 msgid "This recipe requires a username and password" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:377 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:378 msgid "Created by: " msgstr "أنشأه: " -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:415 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:416 msgid "%d days, %d hours and %d minutes ago" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:417 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:418 msgid "Last downloaded" msgstr "آخر تنزيل" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:419 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:420 msgid "Last downloaded: never" msgstr "آخر تنزيل: لم ينزّل من قبل" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:445 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:446 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:162 msgid "Schedule news download" msgstr "جدولة تنزيل الأخبار" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:448 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:449 msgid "Add a custom news source" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:455 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:456 #: /home/kovid/work/calibre/src/calibre/gui2/tags.py:50 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:820 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:824 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:1139 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:838 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:842 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:1157 msgid "News" msgstr "الأخبار" @@ -4474,117 +4473,117 @@ msgstr "" msgid "&Donate to support calibre" msgstr "تبرع& لدعم كاليبر" -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:111 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:113 msgid "&Restart" msgstr "إعادة تشغيل&" -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:145 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:147 msgid "" "

For help visit %s.kovidgoyal.net
" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:148 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:150 msgid "%s: %s by Kovid Goyal %%(version)s
%%(device)s

" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:168 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:170 msgid "Edit metadata individually" msgstr "تحرير الميتاداتا فردياً" -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:170 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:172 msgid "Edit metadata in bulk" msgstr "تحرير الميتاداتا جملةً" -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:173 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:175 msgid "Add books from a single directory" msgstr "إضافة كتب من دليل واحد" -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:174 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:176 msgid "" "Add books from directories, including sub-directories (One book per " "directory, assumes every ebook file is the same book in a different format)" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:177 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:179 msgid "" "Add books from directories, including sub directories (Multiple books per " "directory, assumes every ebook file is a different book)" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:198 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:200 #: /home/kovid/work/calibre/src/calibre/gui2/main_ui.py:360 msgid "Save to disk" msgstr "حفظ إلى القرص" -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:199 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:201 msgid "Save to disk in a single directory" msgstr "حفظ إلى القرص في دليل واحد" -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:200 -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1201 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:202 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1205 msgid "Save only %s format to disk" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:204 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:206 #: /home/kovid/work/calibre/src/calibre/gui2/main_ui.py:366 msgid "View" msgstr "عرض" -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:205 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:207 msgid "View specific format" msgstr "عرض تهيئة معينة" -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:231 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:233 msgid "Convert individually" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:232 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:234 msgid "Bulk convert" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:234 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:236 msgid "Set defaults for conversion" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:235 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:237 msgid "Set defaults for conversion of comics" msgstr "ضبط الإفتراضي في تحويل الرسومات" -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:274 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:276 msgid "Similar books..." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:328 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:330 msgid "Bad database location" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:331 -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1364 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:333 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1368 msgid "Choose a location for your ebook library." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:504 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:506 msgid "Browse by covers" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:599 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:601 msgid "Device: " msgstr "الجهاز: " -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:601 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:603 msgid " detected." msgstr " تم كشفه." -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:624 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:626 msgid "Connected " msgstr "متصل " -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:636 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:638 msgid "Device database corrupted" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:637 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:639 msgid "" "\n" "

The database of books on the reader is corrupted. Try the " @@ -4600,257 +4599,255 @@ msgid "" " " msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:712 -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:765 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:714 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:767 msgid "Uploading books to device." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:720 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:722 msgid "Books" msgstr "كتب" -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:721 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:723 msgid "EPUB Books" msgstr "كتب EPUB" -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:722 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:724 msgid "LRF Books" msgstr "كتب LRF" -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:723 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:725 msgid "HTML Books" msgstr "كتب HTML" -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:724 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:726 msgid "LIT Books" msgstr "كتب LIT" -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:725 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:727 msgid "MOBI Books" msgstr "كتب MOBI" -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:726 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:728 msgid "Text books" msgstr "كتب نصّية" -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:727 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:729 msgid "PDF Books" msgstr "كتب PDF" -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:728 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:730 msgid "Comics" msgstr "الرسومات" -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:729 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:731 msgid "Archives" msgstr "أرشيفات" -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:750 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:752 msgid "Adding books..." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:786 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:788 msgid "" "The selected books will be permanently deleted and the files removed " "from your computer. Are you sure?" msgstr "الكتب المختارة سوف تحذف تماماً من حاسوبك. هل أنت متأكّد؟" -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:799 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:801 msgid "Deleting books from device." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:832 -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:859 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:834 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:861 msgid "Cannot edit metadata" msgstr "لا يمكن تحرير الميتاداتا" -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:833 -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:860 -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:883 -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:973 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:835 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:862 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:885 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:975 msgid "No books selected" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:882 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:884 msgid "Cannot save to disk" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:887 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:889 msgid "Saving to disk..." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:892 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:894 msgid "Saved" msgstr "تم الحفظ" -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:899 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:901 msgid "Choose destination directory" msgstr "إختيار دليل الوجهة" -#: /home/kovid/work/calibre/src/calibre/gui2/main.py:914 +#: /home/kovid/work/calibre/src/calibre/gui2/main.py:916 msgid "" "

Could not save the following books to disk, because the %s format is not " "available for them: