From 66b6c70f21e0cb1d412b0b6e5df37c1bc659a2b0 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 22 Sep 2008 22:06:32 -0700 Subject: [PATCH] Add support for EPUB to the GUI. You can now select EPUB as an output format in the config dialog. This causes all conversion/news download operations to output EPUB files. --- src/calibre/ebooks/epub/__init__.py | 4 +- src/calibre/ebooks/epub/from_any.py | 2 +- src/calibre/ebooks/epub/from_feeds.py | 4 +- src/calibre/ebooks/lrf/feeds/convert_from.py | 1 - src/calibre/gui2/__init__.py | 3 +- src/calibre/gui2/dialogs/config.py | 11 +- src/calibre/gui2/dialogs/config.ui | 45 +- src/calibre/gui2/dialogs/epub.py | 258 +++++++ src/calibre/gui2/dialogs/epub.ui | 735 +++++++++++++++++++ src/calibre/gui2/images/mimetypes/epub.svg | 23 + src/calibre/gui2/main.py | 219 +----- src/calibre/gui2/tools.py | 380 ++++++++++ src/calibre/linux.py | 5 +- src/calibre/parallel.py | 10 + src/calibre/utils/config.py | 4 +- 15 files changed, 1504 insertions(+), 200 deletions(-) create mode 100644 src/calibre/gui2/dialogs/epub.py create mode 100644 src/calibre/gui2/dialogs/epub.ui create mode 100644 src/calibre/gui2/images/mimetypes/epub.svg create mode 100644 src/calibre/gui2/tools.py diff --git a/src/calibre/ebooks/epub/__init__.py b/src/calibre/ebooks/epub/__init__.py index d33582ac8b..93d5813fba 100644 --- a/src/calibre/ebooks/epub/__init__.py +++ b/src/calibre/ebooks/epub/__init__.py @@ -62,10 +62,10 @@ def config(defaults=None): c.add_opt('override_css', ['--override-css'], default=None, help=_('Either the path to a CSS stylesheet or raw CSS. This CSS will override any existing CSS declarations in the source files.')) structure = c.add_group('structure detection', _('Control auto-detection of document structure.')) - structure('chapter', ['--chapter'], default="//*[re:match(name(), 'h[1-2]') and re:test(., 'chapter|book|section', 'i')]", + structure('chapter', ['--chapter'], default="//*[re:match(name(), 'h[1-2]') and re:test(., 'chapter|book|section|part', 'i')]", help=_('''\ An XPath expression to detect chapter titles. The default is to consider

or -

tags that contain the text "chapter" or "book" or "section" as chapter titles. +

tags that contain the words "chapter","book","section" or "part" as chapter titles. 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. diff --git a/src/calibre/ebooks/epub/from_any.py b/src/calibre/ebooks/epub/from_any.py index be1d69dba8..0246ca9b3a 100644 --- a/src/calibre/ebooks/epub/from_any.py +++ b/src/calibre/ebooks/epub/from_any.py @@ -74,7 +74,7 @@ MAP = { 'txt' : txt2opf, 'pdf' : pdf2opf, } - +SOURCE_FORMATS = ['lit', 'mobi', 'prc', 'fb2', 'rtf', 'txt', 'pdf'] def unarchive(path, tdir): extract(path, tdir) diff --git a/src/calibre/ebooks/epub/from_feeds.py b/src/calibre/ebooks/epub/from_feeds.py index 9e90b85469..71ffdefe86 100644 --- a/src/calibre/ebooks/epub/from_feeds.py +++ b/src/calibre/ebooks/epub/from_feeds.py @@ -53,14 +53,14 @@ def convert(opts, recipe_arg, notification=None): html2epub(opf, opts, notification=notification) -def main(args=sys.argv): +def main(args=sys.argv, notification=None, handler=None): parser = option_parser() opts, args = parser.parse_args(args) if len(args) != 2 and opts.feeds is None: parser.print_help() return 1 recipe_arg = args[1] if len(args) > 1 else None - convert(opts, recipe_arg) + convert(opts, recipe_arg, notification=notification) return 0 diff --git a/src/calibre/ebooks/lrf/feeds/convert_from.py b/src/calibre/ebooks/lrf/feeds/convert_from.py index d0ff6ad08e..6965ea7bf3 100644 --- a/src/calibre/ebooks/lrf/feeds/convert_from.py +++ b/src/calibre/ebooks/lrf/feeds/convert_from.py @@ -10,7 +10,6 @@ from calibre.web.feeds.main import option_parser as feeds_option_parser from calibre.web.feeds.main import run_recipe from calibre.ptempfile import TemporaryDirectory from calibre import sanitize_file_name, strftime -from calibre.ebooks import ConversionError import sys, os diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index 1681eb6ff4..b77b270054 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -196,6 +196,7 @@ class FileIconProvider(QFileIconProvider): 'prc' : 'mobi', 'azw' : 'mobi', 'mobi' : 'mobi', + 'epub' : 'epub', } def __init__(self): @@ -222,7 +223,7 @@ class FileIconProvider(QFileIconProvider): return icon def icon_from_ext(self, ext): - key = self.key_from_ext(ext) + key = self.key_from_ext(ext.lower() if ext else '') return self.cached_icon(key) def load_icon(self, fileinfo): diff --git a/src/calibre/gui2/dialogs/config.py b/src/calibre/gui2/dialogs/config.py index 2cc0cec4a7..8fa791fd44 100644 --- a/src/calibre/gui2/dialogs/config.py +++ b/src/calibre/gui2/dialogs/config.py @@ -7,7 +7,7 @@ from PyQt4.QtCore import SIGNAL, QTimer, Qt, QSize, QVariant from calibre import islinux from calibre.gui2.dialogs.config_ui import Ui_Dialog -from calibre.gui2 import qstring_to_unicode, choose_dir, error_dialog, config +from calibre.gui2 import qstring_to_unicode, choose_dir, error_dialog, config, warning_dialog from calibre.utils.config import prefs from calibre.gui2.widgets import FilenamePattern from calibre.ebooks import BOOK_EXTENSIONS @@ -78,6 +78,10 @@ class ConfigDialog(QDialog, Ui_Dialog): items.sort(cmp=lambda x, y: cmp(x[1], y[1])) for item in items: self.language.addItem(item[1], QVariant(item[0])) + + self.output_format.setCurrentIndex(0 if prefs['output_format'] == 'LRF' else 1) + + def compact(self, toggled): d = Vacuum(self, self.db) @@ -112,6 +116,11 @@ class ConfigDialog(QDialog, Ui_Dialog): config['save_to_disk_single_format'] = BOOK_EXTENSIONS[self.single_format.currentIndex()] config['cover_flow_queue_length'] = self.cover_browse.value() prefs['language'] = str(self.language.itemData(self.language.currentIndex()).toString()) + of = str(self.output_format.currentText()) + if of != prefs['output_format'] and 'epub' in of.lower(): + warning_dialog(self, 'Warning', + '

EPUB support is still in beta. If you find bugs, please report them by opening a ticket.').exec_() + prefs['output_format'] = of if not path or not os.path.exists(path) or not os.path.isdir(path): d = error_dialog(self, _('Invalid database location'), diff --git a/src/calibre/gui2/dialogs/config.ui b/src/calibre/gui2/dialogs/config.ui index 35b990efde..bc5f634af4 100644 --- a/src/calibre/gui2/dialogs/config.ui +++ b/src/calibre/gui2/dialogs/config.ui @@ -7,7 +7,7 @@ 0 0 709 - 687 + 723 @@ -160,7 +160,7 @@ - + Format for &single file save: @@ -170,10 +170,10 @@ - + - + &Priority for conversion jobs: @@ -183,10 +183,10 @@ - + - + Default network &timeout: @@ -196,7 +196,7 @@ - + Set the default timeout for network fetches (i.e. anytime we go out to the internet to get information) @@ -215,10 +215,10 @@ - + - + Choose &language (requires restart): @@ -228,6 +228,33 @@ + + + + The default output format for ebook conversions. + + + + LRF + + + + + EPUB + + + + + + + + &Output format: + + + output_format + + + diff --git a/src/calibre/gui2/dialogs/epub.py b/src/calibre/gui2/dialogs/epub.py new file mode 100644 index 0000000000..d047506280 --- /dev/null +++ b/src/calibre/gui2/dialogs/epub.py @@ -0,0 +1,258 @@ +#!/usr/bin/env python +__license__ = 'GPL v3' +__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' +__docformat__ = 'restructuredtext en' + +''' +The GUI for conversion to EPUB. +''' +import os +from PyQt4.Qt import QDialog, QSpinBox, QDoubleSpinBox, QComboBox, QLineEdit, \ + QTextEdit, QCheckBox, Qt, QPixmap, QIcon, QListWidgetItem, SIGNAL + +from calibre.gui2.dialogs.choose_format import ChooseFormatDialog +from calibre.gui2.dialogs.epub_ui import Ui_Dialog +from calibre.gui2 import error_dialog, choose_images, pixmap_to_data +from calibre.ebooks.epub.from_any import SOURCE_FORMATS, config +from calibre.ebooks.metadata import MetaInformation +from calibre.ptempfile import PersistentTemporaryFile +from calibre.ebooks.metadata.opf import OPFCreator + +class Config(QDialog, Ui_Dialog): + + def __init__(self, parent, db, row=None): + QDialog.__init__(self, parent) + self.setupUi(self) + self.connect(self.category_list, SIGNAL('itemEntered(QListWidgetItem *)'), + self.show_category_help) + self.connect(self.cover_button, SIGNAL("clicked()"), self.select_cover) + + self.cover_changed = False + self.db = db + self.id = None + self.row = row + if row is not None: + self.id = db.id(row) + base = config().as_string() + '\n\n' + defaults = self.db.conversion_options(self.id, 'epub') + defaults = base + (defaults if defaults else '') + self.config = config(defaults=defaults) + else: + self.config = config() + self.initialize() + self.get_source_format() + self.category_list.setCurrentRow(0) + if self.row is None: + self.setWindowTitle(_('Bulk convert to EPUB')) + else: + self.setWindowTitle(_(u'Convert %s to EPUB')%unicode(self.title.text())) + + def initialize(self): + self.__w = [] + self.__w.append(QIcon(':/images/dialog_information.svg')) + self.item1 = QListWidgetItem(self.__w[-1], _('Metadata'), self.category_list) + self.__w.append(QIcon(':/images/lookfeel.svg')) + self.item2 = QListWidgetItem(self.__w[-1], _('Look & Feel'), self.category_list) + self.__w.append(QIcon(':/images/page.svg')) + self.item3 = QListWidgetItem(self.__w[-1], _('Page Setup'), self.category_list) + self.__w.append(QIcon(':/images/chapters.svg')) + self.item4 = QListWidgetItem(self.__w[-1], _('Chapter Detection'), self.category_list) + self.setup_tooltips() + self.initialize_options() + + def set_help(self, msg): + if msg and getattr(msg, 'strip', lambda:True)(): + self.help_view.setPlainText(msg) + + def setup_tooltips(self): + for opt in self.config.option_set.preferences: + g = getattr(self, 'opt_'+opt.name, False) + if opt.help and g: + help = opt.help.replace('%default', str(opt.default)) + g._help = help + g.setToolTip(help.replace('<', '<').replace('>', '>')) + g.setWhatsThis(help.replace('<', '<').replace('>', '>')) + g.__class__.enterEvent = lambda obj, event: self.set_help(getattr(obj, '_help', obj.toolTip())) + + def show_category_help(self, item): + text = unicode(item.text()) + help = { + _('Metadata') : _('Specify metadata such as title and author for the book.\n\nMetadata will be updated in the database as well as the generated EPUB file.'), + _('Look & Feel') : _('Adjust the look of the generated EPUB file by specifying things like font sizes.'), + _('Page Setup') : _('Specify the page layout settings like margins.'), + _('Chapter Detection') : _('Fine tune the detection of chapter and section headings.'), + } + self.set_help(help[text]) + + def select_cover(self): + files = choose_images(self, 'change cover dialog', + _('Choose cover for ') + unicode(self.gui_title.text())) + if not files: + return + _file = files[0] + if _file: + _file = os.path.abspath(_file) + if not os.access(_file, os.R_OK): + d = error_dialog(self.window, _('Cannot read'), + _('You do not have permission to read the file: ') + _file) + d.exec_() + return + cf, cover = None, None + try: + cf = open(_file, "rb") + cover = cf.read() + except IOError, e: + d = error_dialog(self.window, _('Error reading file'), + _("

There was an error reading from file:
") + _file + "


"+str(e)) + d.exec_() + if cover: + pix = QPixmap() + pix.loadFromData(cover) + if pix.isNull(): + d = error_dialog(self.window, _file + _(" is not a valid picture")) + d.exec_() + else: + self.cover_path.setText(_file) + self.cover.setPixmap(pix) + self.cover_changed = True + self.cpixmap = pix + + def initialize_metadata_options(self): + all_series = self.db.all_series() + all_series.sort(cmp=lambda x, y : cmp(x[1], y[1])) + for series in all_series: + self.series.addItem(series[1]) + self.series.setCurrentIndex(-1) + + if self.row is not None: + mi = self.db.get_metadata(self.id, index_is_id=True) + self.title.setText(mi.title) + self.author.setText(', '.join(mi.authors)) + self.publisher.setText(mi.publisher if mi.publisher else '') + self.author_sort.setText(mi.author_sort if mi.author_sort else '') + self.tags.setText(', '.join(mi.tags if mi.tags else [])) + self.comment.setText(mi.comments if mi.comments else '') + if mi.series: + self.series.setCurrentIndex(self.series.findText(mi.series)) + if mi.series_index is not None: + self.series_index.setValue(mi.series_index) + + cover = self.db.cover(self.id, index_is_id=True) + if cover: + pm = QPixmap() + pm.loadFromData(cover) + if not pm.isNull(): + self.cover.setPixmap(pm) + + def get_title_and_authors(self): + title = unicode(self.title.text()).strip() + if not title: + title = _('Unknown') + authors = [i.strip() for i in unicode(self.author.text()).strip().split(',')] + if not authors: + authors = [_('Unknown')] + return title, authors + + def get_metadata(self): + title, authors = self.get_title_and_authors() + mi = MetaInformation(title, authors) + publisher = unicode(self.publisher.text()) + if publisher: + mi.publisher = publisher + author_sort = unicode(self.publisher.text()) + if author_sort: + mi.author_sort = author_sort + comments = unicode(self.comment.toPlainText()) + if comments: + mi.comments = comments + mi.series_index = int(self.series_index.value()) + if self.series.currentIndex() > -1: + mi.series = unicode(self.series.currentText()) + tags = [t.strip() for t in unicode(self.tags.text()).split(',')] + if tags: + mi.tags = tags + + return mi + + def read_settings(self): + for pref in self.config.option_set.preferences: + g = getattr(self, 'opt_'+pref.name, False) + if g: + if isinstance(g, (QSpinBox, QDoubleSpinBox)): + self.config.set(pref.name, g.value()) + elif isinstance(g, (QLineEdit, QTextEdit)): + func = getattr(g, 'toPlainText', getattr(g, 'text', None))() + val = unicode(func) + self.config.set(pref.name, val if val else None) + elif isinstance(g, QComboBox): + self.config.set(pref.name, unicode(g.currentText())) + elif isinstance(g, QCheckBox): + self.config.set(pref.name, bool(g.isChecked())) + if self.row is not None: + self.db.set_conversion_options(self.id, 'epub', self.config.src) + + + def initialize_options(self): + self.initialize_metadata_options() + values = self.config.parse() + for pref in self.config.option_set.preferences: + g = getattr(self, 'opt_'+pref.name, False) + if g: + val = getattr(values, pref.name) + if val is None: + continue + if isinstance(g, (QSpinBox, QDoubleSpinBox)): + g.setValue(val) + elif isinstance(g, (QLineEdit, QTextEdit)): + getattr(g, 'setPlainText', g.setText)(val) + elif isinstance(g, QComboBox): + for value in pref.choices: + g.addItem(value) + g.setCurrentIndex(g.findText(val)) + elif isinstance(g, QCheckBox): + g.setCheckState(Qt.Checked if bool(val) else Qt.Unchecked) + + + def get_source_format(self): + self.source_format = None + if self.row is not None: + temp = self.db.formats(self.id, index_is_id=True) + if not temp: + error_dialog(self.parent(), _('Cannot convert'), + _('This book has no available formats')).exec_() + + available_formats = [f.upper().strip() for f in temp.split(',')] + choices = [fmt.upper() for fmt in SOURCE_FORMATS if fmt.upper() in available_formats] + if not choices: + error_dialog(self.parent(), _('No available formats'), + _('Cannot convert %s as this book has no supported formats')%(self.title.text())).exec_() + elif len(choices) == 1: + self.source_format = choices[0] + else: + d = ChooseFormatDialog(self.parent(), _('Choose the format to convert to EPUB'), choices) + if d.exec_() == QDialog.Accepted: + self.source_format = d.format() + + def accept(self): + mi = self.get_metadata() + self.read_settings() + self.cover_file = None + if self.row is not None: + self.db.set_metadata(self.id, mi) + self.mi = self.db.get_metadata(self.id, index_is_id=True) + opf = OPFCreator(os.getcwdu(), self.mi) + self.opf_file = PersistentTemporaryFile('.opf') + opf.render(self.opf_file) + self.opf_file.close() + if self.cover_changed: + self.db.set_cover(self.id, pixmap_to_data(self.cover.pixmap())) + cover = self.db.cover(self.id, index_is_id=True) + if cover: + cf = PersistentTemporaryFile('.jpeg') + cf.write(cover) + cf.close() + self.cover_file = cf + self.opts = self.config.parse() + QDialog.accept(self) + + \ No newline at end of file diff --git a/src/calibre/gui2/dialogs/epub.ui b/src/calibre/gui2/dialogs/epub.ui new file mode 100644 index 0000000000..32d0d40252 --- /dev/null +++ b/src/calibre/gui2/dialogs/epub.ui @@ -0,0 +1,735 @@ + + Dialog + + + + 0 + 0 + 868 + 670 + + + + Convert to EPUB + + + + :/images/convert.svg:/images/convert.svg + + + true + + + + + + + + + 172 + 16777215 + + + + + 75 + true + + + + true + + + Qt::ScrollBarAlwaysOff + + + Qt::ScrollBarAlwaysOff + + + true + + + + 48 + 48 + + + + QListView::TopToBottom + + + false + + + 10 + + + QListView::IconMode + + + true + + + -1 + + + + + + + 3 + + + + + + + + + Book Cover + + + + + + + + + + + :/images/book.svg + + + true + + + Qt::AlignCenter + + + + + + + + + Use cover from &source file + + + true + + + + + + + 6 + + + 0 + + + + + Change &cover image: + + + cover_path + + + + + + + 6 + + + 0 + + + + + true + + + + + + + Browse for an image to use as the cover of this book. + + + ... + + + + :/images/document_open.svg:/images/document_open.svg + + + + + + + + + opt_prefer_metadata_cover + + + + + + + + + + + &Title: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + title + + + + + + + Change the title of this book + + + + + + + &Author(s): + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + author + + + + + + + + 1 + 0 + + + + Change the author(s) of this book. Multiple authors should be separated by a comma + + + + + + + Author So&rt: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + author_sort + + + + + + + + 0 + 0 + + + + Change the author(s) of this book. Multiple authors should be separated by a comma + + + + + + + &Publisher: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + publisher + + + + + + + Change the publisher of this book + + + + + + + Ta&gs: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + tags + + + + + + + Tags categorize the book. This is particularly useful while searching. <br><br>They can be any words or phrases, separated by commas. + + + + + + + &Series: + + + Qt::PlainText + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + series + + + + + + + + 10 + 0 + + + + List of known series. You can add new series. + + + List of known series. You can add new series. + + + true + + + QComboBox::InsertAlphabetically + + + QComboBox::AdjustToContents + + + + + + + true + + + Series index. + + + Series index. + + + Book + + + 1 + + + 10000 + + + + + + + + + + 0 + 0 + + + + + 16777215 + 200 + + + + Comments + + + + + + + 16777215 + 180 + + + + + + + + + + + + + + + + + + + Source en&coding: + + + opt_encoding + + + + + + + + + + Override &CSS + + + + + + + + + + + + + + + + &Profile: + + + opt_profile + + + + + + + -1 + + + 1 + + + + + + + &Left Margin: + + + opt_margin_left + + + + + + + pt + + + 200 + + + 20 + + + + + + + &Right Margin: + + + opt_margin_right + + + + + + + pt + + + 200 + + + 20 + + + + + + + &Top Margin: + + + opt_margin_top + + + + + + + pt + + + 200 + + + 10 + + + + + + + &Bottom Margin: + + + opt_margin_bottom + + + + + + + pt + + + 200 + + + 0 + + + + + + + + + + + Automatic &chapter detection + + + + + + &XPath: + + + opt_chapter + + + + + + + + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'DejaVu Sans'; font-size:10pt; font-weight:400; font-style:normal;"> +<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">You can control how calibre detects chapters using a XPath expression. To learn how to use XPath expressions see the <a href="https://calibre.kovidgoyal.net/user_manual/xpath.html"><span style=" text-decoration: underline; color:#0000ff;">XPath tutorial</span></a></p></body></html> + + + Qt::RichText + + + true + + + + + + + + + + Chapter &mark: + + + opt_chapter_mark + + + + + label_17 + opt_chapter + label_8 + opt_chapter_mark + label_9 + verticalSpacer + + + + + + Automatic &Table of Contents + + + + + + + + + Number of &links to add to Table of Contents + + + opt_max_toc_links + + + + + + + Do not add &detected chapters ot the Table of Contents + + + + + + + + + + Table of Contents &recursion + + + opt_max_toc_recursion + + + + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + 16777215 + 100 + + + + false + + + + + + + + ImageView + QLabel +
widgets.h
+
+
+ + + + + + + buttonBox + accepted() + Dialog + accept() + + + 222 + 652 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 290 + 658 + + + 286 + 274 + + + + + category_list + currentRowChanged(int) + stack + setCurrentIndex(int) + + + 88 + 42 + + + 659 + 12 + + + + +
diff --git a/src/calibre/gui2/images/mimetypes/epub.svg b/src/calibre/gui2/images/mimetypes/epub.svg new file mode 100644 index 0000000000..cafef17b80 --- /dev/null +++ b/src/calibre/gui2/images/mimetypes/epub.svg @@ -0,0 +1,23 @@ + + + + + + + + diff --git a/src/calibre/gui2/main.py b/src/calibre/gui2/main.py index 0811e10e18..af81ea91c5 100644 --- a/src/calibre/gui2/main.py +++ b/src/calibre/gui2/main.py @@ -34,18 +34,16 @@ from calibre.gui2.dialogs.metadata_single import MetadataSingleDialog from calibre.gui2.dialogs.metadata_bulk import MetadataBulkDialog from calibre.gui2.dialogs.jobs import JobsDialog from calibre.gui2.dialogs.conversion_error import ConversionErrorDialog -from calibre.gui2.dialogs.lrf_single import LRFSingleDialog, LRFBulkDialog +from calibre.gui2.tools import convert_single_ebook, convert_bulk_ebooks, set_conversion_defaults, fetch_news from calibre.gui2.dialogs.config import ConfigDialog from calibre.gui2.dialogs.search import SearchDialog from calibre.gui2.dialogs.user_profiles import UserProfiles -import calibre.gui2.dialogs.comicconf as ComicConf from calibre.gui2.dialogs.choose_format import ChooseFormatDialog from calibre.gui2.dialogs.book_info import BookInfo from calibre.ebooks.metadata.meta import set_metadata from calibre.ebooks.metadata import MetaInformation from calibre.ebooks.html import gui_main as html2oeb from calibre.ebooks import BOOK_EXTENSIONS -from calibre.ebooks.lrf import preferred_source_formats as LRF_PREFERRED_SOURCE_FORMATS from calibre.library.database2 import LibraryDatabase2, CoverCache from calibre.parallel import JobKilled from calibre.utils.filenames import ascii_filename @@ -180,7 +178,7 @@ class Main(MainWindow, Ui_MainWindow): cm.addAction(_('Convert individually')) cm.addAction(_('Bulk convert')) cm.addSeparator() - cm.addAction(_('Set defaults for conversion to LRF')) + cm.addAction(_('Set defaults for conversion')) cm.addAction(_('Set defaults for conversion of comics')) self.action_convert.setMenu(cm) QObject.connect(cm.actions()[0], SIGNAL('triggered(bool)'), self.convert_single) @@ -795,21 +793,16 @@ class Main(MainWindow, Ui_MainWindow): self.news_menu.set_custom_feeds(feeds) def fetch_news(self, data): - pt = PersistentTemporaryFile(suffix='_feeds2lrf.lrf') - pt.close() - args = ['feeds2lrf', '--output', pt.name, '--debug'] - if data['username']: - args.extend(['--username', data['username']]) - if data['password']: - args.extend(['--password', data['password']]) - args.append(data['script'] if data['script'] else data['title']) - job = self.job_manager.run_job(Dispatcher(self.news_fetched), 'feeds2lrf', args=[args], - description=_('Fetch news from ')+data['title']) - self.conversion_jobs[job] = (pt, 'lrf') + func, args, desc, fmt, temp_files = fetch_news(data) + self.status_bar.showMessage(_('Fetching news from ')+data['title'], 2000) + job = self.job_manager.run_job(Dispatcher(self.news_fetched), func, args=args, + description=desc) + self.conversion_jobs[job] = (temp_files, fmt) self.status_bar.showMessage(_('Fetching news from ')+data['title'], 2000) def news_fetched(self, job): - pt, fmt = self.conversion_jobs.pop(job) + temp_files, fmt = self.conversion_jobs.pop(job) + pt = temp_files[0] if job.exception is not None: self.job_exception(job) return @@ -820,7 +813,9 @@ class Main(MainWindow, Ui_MainWindow): self.persistent_files.append(pt) try: if not to_device: - os.remove(pt.name) + for f in temp_files: + if os.path.exists(f.name): + os.remove(f.name) except: pass @@ -846,193 +841,55 @@ class Main(MainWindow, Ui_MainWindow): others.append(r) return comics, others - def convert_bulk_others(self, rows): - d = LRFBulkDialog(self) - d.exec_() - if d.result() != QDialog.Accepted: - return - bad_rows = [] - - self.status_bar.showMessage(_('Starting Bulk conversion of %d books')%len(rows), 2000) - if rows and hasattr(rows[0], 'row'): - rows = [r.row() for r in rows] - for i, row in enumerate(rows): - cmdline = list(d.cmdline) - mi = self.library_view.model().db.get_metadata(row) - if mi.title: - cmdline.extend(['--title', mi.title]) - if mi.authors: - cmdline.extend(['--author', ','.join(mi.authors)]) - if mi.publisher: - cmdline.extend(['--publisher', mi.publisher]) - if mi.comments: - cmdline.extend(['--comment', mi.comments]) - data = None - for fmt in LRF_PREFERRED_SOURCE_FORMATS: - try: - data = self.library_view.model().db.format(row, fmt.upper()) - break - except: - continue - if data is None: - bad_rows.append(row) - continue - pt = PersistentTemporaryFile('.'+fmt.lower()) - pt.write(data) - pt.close() - of = PersistentTemporaryFile('.lrf') - of.close() - cover = self.library_view.model().db.cover(row) - cf = None - if cover: - cf = PersistentTemporaryFile('.jpeg') - cf.write(cover) - cf.close() - cmdline.extend(['--cover', cf.name]) - cmdline.extend(['-o', of.name]) - cmdline.append(pt.name) - job = self.job_manager.run_job(Dispatcher(self.book_converted), - 'any2lrf', args=[cmdline], - description=_('Convert book %d of %d (%s)')%(i+1, len(rows), repr(mi.title))) - - - self.conversion_jobs[job] = (cf, pt, of, d.output_format, - self.library_view.model().db.id(row)) - res = [] - for row in bad_rows: - title = self.library_view.model().db.title(row) - res.append('
  • %s
  • '%title) - if res: - msg = _('

    Could not convert %d of %d books, because no suitable source format was found.

    ')%(len(res), len(rows), '\n'.join(res)) - warning_dialog(self, _('Could not convert some books'), msg).exec_() - def convert_bulk(self, checked): - comics, others = self.get_books_for_conversion() - if others: - self.convert_bulk_others(others) - if comics: - opts = ComicConf.get_bulk_conversion_options(self) - if opts: - for i, row in enumerate(comics): - options = opts.copy() - mi = self.library_view.model().db.get_metadata(row) - if mi.title: - options.title = mi.title - if mi.authors: - options.author = ','.join(mi.authors) - data = None - for fmt in ['cbz', 'cbr']: - try: - data = self.library_view.model().db.format(row, fmt.upper()) - if data: - break - except: - continue - - pt = PersistentTemporaryFile('.'+fmt.lower()) - pt.write(data) - pt.close() - of = PersistentTemporaryFile('.lrf') - of.close() - setattr(options, 'output', of.name) - options.verbose = 1 - args = [pt.name, options] - job = self.job_manager.run_job(Dispatcher(self.book_converted), - 'comic2lrf', args=args, - description=_('Convert comic %d of %d (%s)')%(i+1, len(comics), repr(options.title))) - self.conversion_jobs[job] = (None, pt, of, 'lrf', - self.library_view.model().db.id(row)) - - - def set_conversion_defaults(self, checked): - d = LRFSingleDialog(self, None, None) - d.exec_() + r = self.get_books_for_conversion() + if r is None: + return + comics, others = r - def set_comic_conversion_defaults(self, checked): - ComicConf.set_conversion_defaults(self) - - def convert_single_others(self, rows): - changed = False - for row in rows: - d = LRFSingleDialog(self, self.library_view.model().db, row) - if d.selected_format: - d.exec_() - if d.result() == QDialog.Accepted: - cmdline = d.cmdline - data = self.library_view.model().db.format(row, d.selected_format) - pt = PersistentTemporaryFile('.'+d.selected_format.lower()) - pt.write(data) - pt.close() - of = PersistentTemporaryFile('.lrf') - of.close() - cmdline.extend(['-o', of.name]) - cmdline.append(pt.name) - job = self.job_manager.run_job(Dispatcher(self.book_converted), - 'any2lrf', args=[cmdline], - description=_('Convert book: ')+d.title()) - - self.conversion_jobs[job] = (d.cover_file, pt, of, d.output_format, d.id) - changed = True + jobs, changed = convert_bulk_ebooks(self, self.library_view.model().db, comics, others) + for func, args, desc, fmt, id, temp_files in jobs: + job = self.job_manager.run_job(Dispatcher(self.book_converted), + func, args=args, description=desc) + self.conversion_jobs[job] = (temp_files, fmt, id) + if changed: self.library_view.model().resort(reset=False) self.library_view.model().research() + + def set_conversion_defaults(self, checked): + set_conversion_defaults(False, self, self.library_view.model().db) + def set_comic_conversion_defaults(self, checked): + set_conversion_defaults(True, self, self.library_view.model().db) def convert_single(self, checked): - comics, others = self.get_books_for_conversion() - if others: - self.convert_single_others(others) - changed = False - db = self.library_view.model().db - for row in comics: - mi = db.get_metadata(row) - title = author = _('Unknown') - if mi.title: - title = mi.title - if mi.authors: - author = ','.join(mi.authors) - defaults = db.conversion_options(db.id(row), 'comic') - opts, defaults = ComicConf.get_conversion_options(self, defaults, title, author) - if defaults is not None: - db.set_conversion_options(db.id(row), 'comic', defaults) - if opts is None: continue - for fmt in ['cbz', 'cbr']: - try: - data = db.format(row, fmt.upper()) - break - except: - continue - pt = PersistentTemporaryFile('.'+fmt) - pt.write(data) - pt.close() - of = PersistentTemporaryFile('.lrf') - of.close() - opts.output = of.name - opts.verbose = 1 - args = [pt.name, opts] - changed = True + r = self.get_books_for_conversion() + if r is None: return + comics, others = r + jobs, changed = convert_single_ebook(self, self.library_view.model().db, comics, others) + for func, args, desc, fmt, id, temp_files in jobs: job = self.job_manager.run_job(Dispatcher(self.book_converted), - 'comic2lrf', args=args, - description=_('Convert comic: ')+opts.title) - self.conversion_jobs[job] = (None, pt, of, 'lrf', - self.library_view.model().db.id(row)) + func, args=args, description=desc) + self.conversion_jobs[job] = (temp_files, fmt, id) + if changed: self.library_view.model().resort(reset=False) self.library_view.model().research() def book_converted(self, job): - cf, pt, of, fmt, book_id = self.conversion_jobs.pop(job) + temp_files, fmt, book_id = self.conversion_jobs.pop(job) try: if job.exception is not None: self.job_exception(job) return - data = open(of.name, 'rb') + data = open(temp_files[-1].name, 'rb') self.library_view.model().db.add_format(book_id, fmt, data, index_is_id=True) data.close() self.status_bar.showMessage(job.description + (' completed'), 2000) finally: - for f in (cf, of, pt): + for f in temp_files: try: if os.path.exists(f.name): os.remove(f.name) diff --git a/src/calibre/gui2/tools.py b/src/calibre/gui2/tools.py new file mode 100644 index 0000000000..68d90f5228 --- /dev/null +++ b/src/calibre/gui2/tools.py @@ -0,0 +1,380 @@ +#!/usr/bin/env python +__license__ = 'GPL v3' +__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' +__docformat__ = 'restructuredtext en' + +''' +Logic for setting up conversion jobs +''' +import os +from PyQt4.Qt import QDialog + +from calibre.utils.config import prefs +from calibre.gui2.dialogs.lrf_single import LRFSingleDialog, LRFBulkDialog +from calibre.gui2.dialogs.epub import Config as EPUBConvert +import calibre.gui2.dialogs.comicconf as ComicConf +from calibre.gui2 import warning_dialog +from calibre.ptempfile import PersistentTemporaryFile +from calibre.ebooks.lrf import preferred_source_formats as LRF_PREFERRED_SOURCE_FORMATS +from calibre.ebooks.metadata.opf import OPFCreator +from calibre.ebooks.epub.from_any import SOURCE_FORMATS as EPUB_PREFERRED_SOURCE_FORMATS + +def convert_single_epub(parent, db, comics, others): + changed = False + jobs = [] + for row in others: + temp_files = [] + d = EPUBConvert(parent, db, row) + if d.source_format is not None: + d.exec_() + if d.result() == QDialog.Accepted: + opts = d.opts + data = db.format(row, d.source_format) + pt = PersistentTemporaryFile('.'+d.source_format.lower()) + pt.write(data) + pt.close() + of = PersistentTemporaryFile('.epub') + of.close() + opts.output = of.name + opts.from_opf = d.opf_file.name + opts.verbose = 2 + args = [opts, pt.name] + if d.cover_file: + temp_files.append(d.cover_file) + opts.cover = d.cover_file.name + temp_files.extend([d.opf_file, pt, of]) + jobs.append(('any2epub', args, _('Convert book: ')+d.mi.title, + 'EPUB', db.id(row), temp_files)) + changed = True + + for row in comics: + mi = db.get_metadata(row) + title = author = _('Unknown') + if mi.title: + title = mi.title + if mi.authors: + author = ','.join(mi.authors) + defaults = db.conversion_options(db.id(row), 'comic') + opts, defaults = ComicConf.get_conversion_options(parent, defaults, title, author) + if defaults is not None: + db.set_conversion_options(db.id(row), 'comic', defaults) + if opts is None: continue + for fmt in ['cbz', 'cbr']: + try: + data = db.format(row, fmt.upper()) + break + except: + continue + pt = PersistentTemporaryFile('.'+fmt) + pt.write(data) + pt.close() + of = PersistentTemporaryFile('.epub') + of.close() + opts.output = of.name + opts.verbose = 2 + args = [pt.name, opts] + changed = True + jobs.append(('comic2epub', args, _('Convert comic: ')+opts.title, + 'EPUB', db.id(row), [pt, of])) + + return jobs, changed + + + +def convert_single_lrf(parent, db, comics, others): + changed = False + jobs = [] + for row in others: + temp_files = [] + d = LRFSingleDialog(parent, db, row) + if d.selected_format: + d.exec_() + if d.result() == QDialog.Accepted: + cmdline = d.cmdline + data = db.format(row, d.selected_format) + pt = PersistentTemporaryFile('.'+d.selected_format.lower()) + pt.write(data) + pt.close() + of = PersistentTemporaryFile('.lrf') + of.close() + cmdline.extend(['-o', of.name]) + cmdline.append(pt.name) + if d.cover_file: + temp_files.append(d.cover_file) + temp_files.extend([pt, of]) + jobs.append(('any2lrf', [cmdline], _('Convert book: ')+d.title(), + 'LRF', db.id(row), temp_files)) + changed = True + + for row in comics: + mi = db.get_metadata(row) + title = author = _('Unknown') + if mi.title: + title = mi.title + if mi.authors: + author = ','.join(mi.authors) + defaults = db.conversion_options(db.id(row), 'comic') + opts, defaults = ComicConf.get_conversion_options(parent, defaults, title, author) + if defaults is not None: + db.set_conversion_options(db.id(row), 'comic', defaults) + if opts is None: continue + for fmt in ['cbz', 'cbr']: + try: + data = db.format(row, fmt.upper()) + break + except: + continue + pt = PersistentTemporaryFile('.'+fmt) + pt.write(data) + pt.close() + of = PersistentTemporaryFile('.lrf') + of.close() + opts.output = of.name + opts.verbose = 1 + args = [pt.name, opts] + changed = True + jobs.append(('comic2lrf', args, _('Convert comic: ')+opts.title, + 'LRF', db.id(row), [pt, of])) + + return jobs, changed + +def convert_bulk_epub(parent, db, comics, others): + if others: + d = EPUBConvert(parent, db) + if d.exec_() != QDialog.Accepted: + others = [] + else: + opts = d.opts + opts.verbose = 2 + if comics: + comic_opts = ComicConf.get_bulk_conversion_options(parent) + if not comic_opts: + comics = [] + bad_rows = [] + jobs = [] + total = sum(map(len, (others, comics))) + if total == 0: + return + parent.status_bar.showMessage(_('Starting Bulk conversion of %d books')%total, 2000) + + for i, row in enumerate(others+comics): + if row in others: + data = None + for fmt in EPUB_PREFERRED_SOURCE_FORMATS: + try: + data = db.format(row, fmt.upper()) + break + except: + continue + if data is None: + bad_rows.append(row) + continue + options = opts.copy() + mi = db.get_metadata(row) + opf = OPFCreator(os.getcwdu(), mi) + opf_file = PersistentTemporaryFile('.opf') + opf.render(opf_file) + opf_file.close() + pt = PersistentTemporaryFile('.'+fmt.lower()) + pt.write(data) + pt.close() + of = PersistentTemporaryFile('.epub') + of.close() + cover = db.cover(row) + cf = None + if cover: + cf = PersistentTemporaryFile('.jpeg') + cf.write(cover) + cf.close() + options.cover = cf.name + options.output = of.name + options.from_opf = opf_file.name + args = [options, pt.name] + desc = _('Convert book %d of %d (%s)')%(i+1, total, repr(mi.title)) + temp_files = [cf] if cf is not None else [] + temp_files.extend([opf_file, pt, of]) + jobs.append(('any2epub', args, desc, 'EPUB', db.id(row), temp_files)) + else: + options = comic_opts.copy() + mi = db.get_metadata(row) + if mi.title: + options.title = mi.title + if mi.authors: + options.author = ','.join(mi.authors) + data = None + for fmt in ['cbz', 'cbr']: + try: + data = db.format(row, fmt.upper()) + if data: + break + except: + continue + + pt = PersistentTemporaryFile('.'+fmt.lower()) + pt.write(data) + pt.close() + of = PersistentTemporaryFile('.epub') + of.close() + setattr(options, 'output', of.name) + options.verbose = 1 + args = [pt.name, options] + desc = _('Convert book %d of %d (%s)')%(i+1, total, repr(mi.title)) + jobs.append(('comic2epub', args, desc, 'EPUB', db.id(row), [pt, of])) + + if bad_rows: + res = [] + for row in bad_rows: + title = db.title(row) + res.append('
  • %s
  • '%title) + + msg = _('

    Could not convert %d of %d books, because no suitable source format was found.

    ')%(len(res), total, '\n'.join(res)) + warning_dialog(parent, _('Could not convert some books'), msg).exec_() + + return jobs, False + + +def convert_bulk_lrf(parent, db, comics, others): + if others: + d = LRFBulkDialog(parent) + if d.exec_() != QDialog.Accepted: + others = [] + if comics: + comic_opts = ComicConf.get_bulk_conversion_options(parent) + if not comic_opts: + comics = [] + bad_rows = [] + jobs = [] + total = sum(map(len, (others, comics))) + if total == 0: + return + parent.status_bar.showMessage(_('Starting Bulk conversion of %d books')%total, 2000) + + for i, row in enumerate(others+comics): + if row in others: + cmdline = list(d.cmdline) + mi = db.get_metadata(row) + if mi.title: + cmdline.extend(['--title', mi.title]) + if mi.authors: + cmdline.extend(['--author', ','.join(mi.authors)]) + if mi.publisher: + cmdline.extend(['--publisher', mi.publisher]) + if mi.comments: + cmdline.extend(['--comment', mi.comments]) + data = None + for fmt in LRF_PREFERRED_SOURCE_FORMATS: + try: + data = db.format(row, fmt.upper()) + break + except: + continue + if data is None: + bad_rows.append(row) + continue + pt = PersistentTemporaryFile('.'+fmt.lower()) + pt.write(data) + pt.close() + of = PersistentTemporaryFile('.lrf') + of.close() + cover = db.cover(row) + cf = None + if cover: + cf = PersistentTemporaryFile('.jpeg') + cf.write(cover) + cf.close() + cmdline.extend(['--cover', cf.name]) + cmdline.extend(['-o', of.name]) + cmdline.append(pt.name) + desc = _('Convert book %d of %d (%s)')%(i+1, total, repr(mi.title)) + temp_files = [cf] if cf is not None else [] + temp_files.extend([pt, of]) + jobs.append(('any2lrf', [cmdline], desc, 'LRF', db.id(row), temp_files)) + else: + options = comic_opts.copy() + mi = db.get_metadata(row) + if mi.title: + options.title = mi.title + if mi.authors: + options.author = ','.join(mi.authors) + data = None + for fmt in ['cbz', 'cbr']: + try: + data = db.format(row, fmt.upper()) + if data: + break + except: + continue + + pt = PersistentTemporaryFile('.'+fmt.lower()) + pt.write(data) + pt.close() + of = PersistentTemporaryFile('.lrf') + of.close() + setattr(options, 'output', of.name) + options.verbose = 1 + args = [pt.name, options] + desc = _('Convert book %d of %d (%s)')%(i+1, total, repr(mi.title)) + jobs.append(('comic2lrf', args, desc, 'LRF', db.id(row), [pt, of])) + + if bad_rows: + res = [] + for row in bad_rows: + title = db.title(row) + res.append('
  • %s
  • '%title) + + msg = _('

    Could not convert %d of %d books, because no suitable source format was found.

    ')%(len(res), total, '\n'.join(res)) + warning_dialog(parent, _('Could not convert some books'), msg).exec_() + + return jobs, False + +def set_conversion_defaults_lrf(comic, parent, db): + if comic: + ComicConf.set_conversion_defaults(parent) + else: + LRFSingleDialog(parent, None, None).exec_() + +def set_conversion_defaults_epub(comic, parent, db): + if comic: + ComicConf.set_conversion_defaults(parent) + else: + d = EPUBConvert(parent, db) + d.setWindowTitle(_('Set conversion defaults')) + d.exec_() + + +def _fetch_news(data, fmt): + pt = PersistentTemporaryFile(suffix='_feeds2%s.%s'%(fmt.lower(), fmt.lower())) + pt.close() + args = ['feeds2%s'%fmt.lower(), '--output', pt.name, '--debug'] + if data['username']: + args.extend(['--username', data['username']]) + if data['password']: + args.extend(['--password', data['password']]) + args.append(data['script'] if data['script'] else data['title']) + return 'feeds2'+fmt.lower(), [args], _('Fetch news from ')+data['title'], fmt.upper(), [pt] + + +def convert_single_ebook(*args): + fmt = prefs['output_format'].lower() + if fmt == 'lrf': + return convert_single_lrf(*args) + elif fmt == 'epub': + return convert_single_epub(*args) + +def convert_bulk_ebooks(*args): + fmt = prefs['output_format'].lower() + if fmt == 'lrf': + return convert_bulk_lrf(*args) + elif fmt == 'epub': + return convert_bulk_epub(*args) + +def set_conversion_defaults(comic, parent, db): + fmt = prefs['output_format'].lower() + if fmt == 'lrf': + return set_conversion_defaults_lrf(comic, parent, db) + elif fmt == 'epub': + return set_conversion_defaults_epub(comic, parent, db) + +def fetch_news(data): + fmt = prefs['output_format'].lower() + return _fetch_news(data, fmt) \ No newline at end of file diff --git a/src/calibre/linux.py b/src/calibre/linux.py index 6c7e390306..7a820d3cfa 100644 --- a/src/calibre/linux.py +++ b/src/calibre/linux.py @@ -443,7 +443,10 @@ def post_install(): from calibre.utils.config import config_dir if os.path.exists(config_dir): - shutil.rmtree(config_dir) + os.chdir(config_dir) + for f in os.listdir('.'): + if os.stat(f).st_uid == 0: + os.unlink(f) VIEWER = '''\ diff --git a/src/calibre/parallel.py b/src/calibre/parallel.py index 2cf1f50630..c8b8cd029f 100644 --- a/src/calibre/parallel.py +++ b/src/calibre/parallel.py @@ -54,6 +54,16 @@ PARALLEL_FUNCS = { 'comic2lrf' : ('calibre.ebooks.lrf.comic.convert_from', 'do_convert', {}, 'notification'), + + 'any2epub' : + ('calibre.ebooks.epub.from_any', 'any2epub', {}, None), + + 'feeds2epub' : + ('calibre.ebooks.epub.from_feeds', 'main', {}, 'notification'), + + 'comic2epub' : + ('calibre.ebooks.epub.from_comic', 'convert', {}, 'notification'), + } diff --git a/src/calibre/utils/config.py b/src/calibre/utils/config.py index 9aef6b1f66..8aa5ac7991 100644 --- a/src/calibre/utils/config.py +++ b/src/calibre/utils/config.py @@ -322,7 +322,7 @@ class OptionSet(object): prefs = [pref for pref in self.preferences if pref.group == name] lines = ['### Begin group: %s'%(name if name else 'DEFAULT')] if desc: - lines += map(lambda x: '# '+x for x in desc.split('\n')) + lines += map(lambda x: '# '+x, desc.split('\n')) lines.append(' ') for pref in prefs: lines.append('# '+pref.name.replace('_', ' ')) @@ -522,6 +522,8 @@ def _prefs(): help=_('Path to directory in which your library of books is stored')) c.add_opt('language', default=None, help=_('The language in which to display the user interface')) + c.add_opt('output_format', default='LRF', + help=_('The default output format for ebook conversions.')) c.add_opt('migrated', default=False, help='For Internal use. Don\'t modify.') return c