diff --git a/src/calibre/ebooks/__init__.py b/src/calibre/ebooks/__init__.py index 79f4f7631e..416fe61789 100644 --- a/src/calibre/ebooks/__init__.py +++ b/src/calibre/ebooks/__init__.py @@ -59,10 +59,9 @@ 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, \ - QApplication - if QApplication.instance() is None: - QApplication([]) + from PyQt4.Qt import QEventLoop, QPalette, Qt, SIGNAL, QUrl, QSize + from calibre.gui2 import is_ok_to_use_qt + if not is_ok_to_use_qt(): return None 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/epub/input.py b/src/calibre/ebooks/epub/input.py index f134ea6abd..b748429725 100644 --- a/src/calibre/ebooks/epub/input.py +++ b/src/calibre/ebooks/epub/input.py @@ -80,8 +80,10 @@ class EPUBInput(InputFormatPlugin): 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) + renderer = render_html(guide_cover) + if renderer is not None: + open('calibre_raster_cover.jpg', 'wb').write( + renderer.data) def convert(self, stream, options, file_ext, log, accelerators): diff --git a/src/calibre/ebooks/mobi/reader.py b/src/calibre/ebooks/mobi/reader.py index ff398ca3bb..b787ce7e7c 100644 --- a/src/calibre/ebooks/mobi/reader.py +++ b/src/calibre/ebooks/mobi/reader.py @@ -290,14 +290,6 @@ class MobiReader(object): self.replace_page_breaks() self.cleanup_html() - if self.processed_html.startswith('' - self.processed_html = \ - re.compile('', re.IGNORECASE).sub( - '\n\n' - '\t\n', - self.processed_html) - self.log.debug('Parsing HTML...') root = html.fromstring(self.processed_html) if root.xpath('descendant::p/descendant::p'): @@ -305,7 +297,7 @@ class MobiReader(object): self.log.warning('Markup contains unclosed

tags, parsing using', 'BeatifulSoup') root = soupparser.fromstring(self.processed_html) - if root[0].tag != 'html': + if root.tag != 'html': self.log.warn('File does not have opening tag') nroot = html.fromstring('') bod = nroot.find('body') @@ -314,6 +306,35 @@ class MobiReader(object): bod.append(child) root = nroot + htmls = list(root.xpath('//html')) + if len(htmls) > 1: + self.log.warn('Markup contains multiple tags') + # Keep only the largest head and body + bodies, heads = root.xpath('//body'), root.xpath('//head') + def sz(x): return len(list(x.iter())) + def scmp(x, y): return cmp(sz(x), sz(y)) + body = list(sorted(bodies, cmp=scmp)) + head = list(sorted(heads, cmp=scmp)) + for x in root: root.remove(x) + if head: + root.append(head[-1]) + if body: + root.append(body[-1]) + for x in root.xpath('//script'): + x.getparent().remove(x) + + head = root.xpath('//head') + if head: + head = head[0] + else: + head = root.makeelement('head', {}) + root.insert(0, head) + head.text = '\n\t' + link = head.makeelement('link', {'type':'text/css', + 'href':'styles.css'}) + head.insert(0, link) + link.tail = '\n\t' + self.upshift_markup(root) guides = root.xpath('//guide') guide = guides[0] if guides else None diff --git a/src/calibre/ebooks/oeb/transforms/flatcss.py b/src/calibre/ebooks/oeb/transforms/flatcss.py index b2831bf448..c726ae30c9 100644 --- a/src/calibre/ebooks/oeb/transforms/flatcss.py +++ b/src/calibre/ebooks/oeb/transforms/flatcss.py @@ -24,6 +24,19 @@ def asfloat(value, default): value = default return float(value) +def dynamic_rescale_factor(node): + classes = node.get('class', '').split(' ') + classes = [x.replace('calibre_rescale_', '') for x in classes if + x.startswith('calibre_rescale_')] + if not classes: return None + factor = 1.0 + for x in classes: + try: + factor *= float(x)/100. + except ValueError: + continue + return factor + class KeyMapper(object): def __init__(self, sbase, dbase, dkey): @@ -202,11 +215,19 @@ class CSSFlattener(object): if 'bgcolor' in node.attrib: cssdict['background-color'] = node.attrib['bgcolor'] del node.attrib['bgcolor'] - if not self.context.disable_font_rescaling and \ - 'font-size' in cssdict or tag == 'body': - fsize = self.fmap[style['font-size']] - cssdict['font-size'] = "%0.5fem" % (fsize / psize) - psize = fsize + if not self.context.disable_font_rescaling: + _sbase = self.sbase if self.sbase is not None else \ + self.context.source.fbase + dyn_rescale = dynamic_rescale_factor(node) + if dyn_rescale is not None: + fsize = self.fmap[_sbase] + fsize *= dyn_rescale + cssdict['font-size'] = '%0.5fem'%(fsize/psize) + psize = fsize + elif 'font-size' in cssdict or tag == 'body': + fsize = self.fmap[style['font-size']] + cssdict['font-size'] = "%0.5fem" % (fsize / psize) + psize = fsize if cssdict: if self.lineh and self.fbase and tag != 'body': self.clean_edges(cssdict, style, psize) diff --git a/src/calibre/ebooks/oeb/transforms/jacket.py b/src/calibre/ebooks/oeb/transforms/jacket.py index 8c995dadec..c0a656d64d 100644 --- a/src/calibre/ebooks/oeb/transforms/jacket.py +++ b/src/calibre/ebooks/oeb/transforms/jacket.py @@ -25,14 +25,16 @@ class Jacket(object): %(title)s -

-

%(title)s

-

%(jacket)s

-
%(series)s
-
%(tags)s
-
-
- %(comments)s +
+
+

%(title)s

+

%(jacket)s

+
%(series)s
+
%(tags)s
+
+
+ %(comments)s +
diff --git a/src/calibre/ebooks/oeb/transforms/split.py b/src/calibre/ebooks/oeb/transforms/split.py index 86e60a7784..0cc17bd14f 100644 --- a/src/calibre/ebooks/oeb/transforms/split.py +++ b/src/calibre/ebooks/oeb/transforms/split.py @@ -369,13 +369,13 @@ class FlowSplitter(object): for path in ( '//*[re:match(name(), "h[1-6]", "i")]', - '/html/body/div', - '//pre', - '//hr', - '//p', - '//div', - '//br', - '//li', + '/h:html/h:body/h:div', + '//h:pre', + '//h:hr', + '//h:p', + '//h:div', + '//h:br', + '//h:li', ): elems = root.xpath(path, namespaces=NAMESPACES) elem = pick_elem(elems) diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index ebe877f823..2afa65e690 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -27,8 +27,6 @@ def _config(): help=_('Frequently used directories')) c.add_opt('send_to_storage_card_by_default', default=False, help=_('Send file to storage card instead of main memory by default')) - c.add_opt('save_to_disk_single_format', default='lrf', - help=_('The format to use when saving single files to disk')) c.add_opt('confirm_delete', default=False, help=_('Confirm before deleting')) c.add_opt('toolbar_icon_size', default=QSize(48, 48), @@ -100,18 +98,23 @@ def available_width(): def extension(path): return os.path.splitext(path)[1][1:].lower() -def warning_dialog(parent, title, msg, det_msg=''): +def warning_dialog(parent, title, msg, det_msg='', show=False): d = QMessageBox(QMessageBox.Warning, 'WARNING: '+title, msg, QMessageBox.Ok, parent) d.setDetailedText(det_msg) d.setIconPixmap(QPixmap(':/images/dialog_warning.svg')) + + if show: + return d.exec_() return d -def error_dialog(parent, title, msg, det_msg=''): +def error_dialog(parent, title, msg, det_msg='', show=False): d = QMessageBox(QMessageBox.Critical, 'ERROR: '+title, msg, QMessageBox.Ok, parent) d.setDetailedText(det_msg) d.setIconPixmap(QPixmap(':/images/dialog_error.svg')) + if show: + return d.exec_() return d def question_dialog(parent, title, msg, det_msg=''): @@ -121,13 +124,16 @@ def question_dialog(parent, title, msg, det_msg=''): d.setIconPixmap(QPixmap(':/images/dialog_information.svg')) return d -def info_dialog(parent, title, msg, det_msg=''): +def info_dialog(parent, title, msg, det_msg='', show=False): d = QMessageBox(QMessageBox.Information, title, msg, QMessageBox.NoButton, parent) d.setDetailedText(det_msg) d.setIconPixmap(QPixmap(':/images/dialog_information.svg')) + if show: + return d.exec_() return d + def qstring_to_unicode(q): return unicode(q) diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index cf04e18ae6..b3dcf8a21c 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -530,8 +530,8 @@ class DeviceGUI(object): ]) error_dialog(self, _('Failed to email books'), _('Failed to email the following books:'), - '%s'%errors, - show=True) + '%s'%errors + ) else: self.status_bar.showMessage(_('Sent by email:') + ', '.join(good), 5000) diff --git a/src/calibre/gui2/dialogs/config.py b/src/calibre/gui2/dialogs/config.py index 1550912e61..8553f1c875 100644 --- a/src/calibre/gui2/dialogs/config.py +++ b/src/calibre/gui2/dialogs/config.py @@ -399,12 +399,15 @@ class ConfigDialog(QDialog, Ui_Dialog): default_index = self.output_format.findText(prefs['output_format']) self.output_format.setCurrentIndex(default_index if default_index != -1 else 0) - self.book_exts = sorted(BOOK_EXTENSIONS) - for ext in self.book_exts: - self.single_format.addItem(ext.upper(), QVariant(ext)) + output_formats = sorted(available_output_formats()) + output_formats.remove('oeb') + for f in output_formats: + self.output_format.addItem(f.upper()) + default_index = \ + self.output_format.findText(prefs['output_format'].upper()) + self.output_format.setCurrentIndex(default_index if default_index != -1 else 0) + - single_format = config['save_to_disk_single_format'] - self.single_format.setCurrentIndex(self.book_exts.index(single_format)) self.cover_browse.setValue(config['cover_flow_queue_length']) self.systray_notifications.setChecked(not config['disable_tray_notification']) from calibre.translations.compiled import translations @@ -426,17 +429,17 @@ class ConfigDialog(QDialog, Ui_Dialog): self.pdf_metadata.setChecked(prefs['read_file_metadata']) - added_html = False - for ext in self.book_exts: + exts = set([]) + for ext in BOOK_EXTENSIONS: ext = ext.lower() ext = re.sub(r'(x{0,1})htm(l{0,1})', 'html', ext) if ext == 'lrf' or is_supported('book.'+ext): - if ext == 'html' and added_html: - continue - self.viewer.addItem(ext.upper()) - self.viewer.item(self.viewer.count()-1).setFlags(Qt.ItemIsEnabled|Qt.ItemIsUserCheckable) - self.viewer.item(self.viewer.count()-1).setCheckState(Qt.Checked if ext.upper() in config['internally_viewed_formats'] else Qt.Unchecked) - added_html = ext == 'html' + exts.add(ext) + + for ext in sorted(exts): + self.viewer.addItem(ext.upper()) + self.viewer.item(self.viewer.count()-1).setFlags(Qt.ItemIsEnabled|Qt.ItemIsUserCheckable) + self.viewer.item(self.viewer.count()-1).setCheckState(Qt.Checked if ext.upper() in config['internally_viewed_formats'] else Qt.Unchecked) self.viewer.sortItems() self.start.setEnabled(not getattr(self.server, 'is_running', False)) self.test.setEnabled(not self.start.isEnabled()) @@ -767,8 +770,7 @@ class ConfigDialog(QDialog, Ui_Dialog): p = {0:'normal', 1:'high', 2:'low'}[self.priority.currentIndex()] prefs['worker_process_priority'] = p prefs['read_file_metadata'] = bool(self.pdf_metadata.isChecked()) - prefs['output_format'] = self.output_format.currentText() - config['save_to_disk_single_format'] = self.book_exts[self.single_format.currentIndex()] + prefs['output_format'] = unicode(self.output_format.currentText()).upper() config['cover_flow_queue_length'] = self.cover_browse.value() prefs['language'] = str(self.language.itemData(self.language.currentIndex()).toString()) config['systray_icon'] = self.systray_icon.checkState() == Qt.Checked diff --git a/src/calibre/gui2/dialogs/config.ui b/src/calibre/gui2/dialogs/config.ui index 4d8d64e151..bdf9642d3e 100644 --- a/src/calibre/gui2/dialogs/config.ui +++ b/src/calibre/gui2/dialogs/config.ui @@ -147,19 +147,6 @@ - - - Format for &single file save: - - - single_format - - - - - - - Default network &timeout: @@ -169,7 +156,7 @@ - + Set the default timeout for network fetches (i.e. anytime we go out to the internet to get information) @@ -188,10 +175,10 @@ - + - + Choose &language (requires restart): @@ -201,7 +188,7 @@ - + @@ -220,7 +207,7 @@ - + Job &priority: diff --git a/src/calibre/gui2/dialogs/scheduler.py b/src/calibre/gui2/dialogs/scheduler.py index c9cb62cfaa..d757ec75c8 100644 --- a/src/calibre/gui2/dialogs/scheduler.py +++ b/src/calibre/gui2/dialogs/scheduler.py @@ -20,6 +20,7 @@ from calibre.utils.search_query_parser import SearchQueryParser from calibre.utils.pyparsing import ParseException from calibre.gui2 import NONE, error_dialog, config as gconf from calibre.utils.config import DynamicConfig +from calibre.ptempfile import PersistentTemporaryFile from calibre.gui2.dialogs.user_profiles import UserProfiles config = DynamicConfig('scheduler') @@ -522,8 +523,12 @@ class Scheduler(QObject): self.recipes.remove(recipe) save_recipes(self.recipes) return + pt = PersistentTemporaryFile('_builtin.recipe') + pt.write(script) + pt.close() + script = pt.name except ValueError: - script = recipe.title + script = recipe.title + '.recipe' self.debug('\tQueueing:', recipe) self.main.download_scheduled_recipe(recipe, script, self.recipe_downloaded) self.queue.add(recipe) diff --git a/src/calibre/gui2/main.py b/src/calibre/gui2/main.py index 6840f4d7a8..33e985a0d1 100644 --- a/src/calibre/gui2/main.py +++ b/src/calibre/gui2/main.py @@ -47,6 +47,19 @@ from calibre.library.database2 import LibraryDatabase2, CoverCache from calibre.parallel import JobKilled from calibre.gui2.dialogs.confirm_delete import confirm +class SaveMenu(QMenu): + + def __init__(self, parent): + QMenu.__init__(self, _('Save single format to disk...'), parent) + for ext in sorted(BOOK_EXTENSIONS): + action = self.addAction(ext.upper()) + setattr(self, 'do_'+ext, partial(self.do, ext)) + self.connect(action, SIGNAL('triggered(bool)'), + getattr(self, 'do_'+ext)) + + def do(self, ext, *args): + self.emit(SIGNAL('save_fmt(PyQt_PyObject)'), ext) + class Main(MainWindow, Ui_MainWindow, DeviceGUI): 'The main GUI' @@ -201,8 +214,12 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): self.save_menu = QMenu() self.save_menu.addAction(_('Save to disk')) self.save_menu.addAction(_('Save to disk in a single directory')) - self.save_menu.addAction(_('Save only %s format to disk')%\ - config.get('save_to_disk_single_format').upper()) + self.save_menu.addAction(_('Save only %s format to disk')% + prefs['output_format'].upper()) + self.save_sub_menu = SaveMenu(self) + self.save_menu.addMenu(self.save_sub_menu) + self.connect(self.save_sub_menu, SIGNAL('save_fmt(PyQt_PyObject)'), + self.save_specific_format_disk) self.view_menu = QMenu() self.view_menu.addAction(_('View')) @@ -856,10 +873,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): _('Failed to download metadata for the following:'), details, self).exec_() else: - err = _('Failed to download metadata:')+\ - '
'+x.tb+'
' - error_dialog(self, _('Error'), err, - show=True) + err = _('Failed to download metadata:') + error_dialog(self, _('Error'), err, det_msg=x.tb).exec_() @@ -912,7 +927,10 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): ############################## Save to disk ################################ def save_single_format_to_disk(self, checked): - self.save_to_disk(checked, True, config['save_to_disk_single_format']) + self.save_to_disk(checked, True, prefs['output_format']) + + def save_specific_format_disk(self, fmt): + self.save_to_disk(False, True, fmt) def save_to_single_dir(self, checked): self.save_to_disk(checked, True) @@ -921,10 +939,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): rows = self.current_view().selectionModel().selectedRows() if not rows or len(rows) == 0: - d = error_dialog(self, _('Cannot save to disk'), - _('No books selected')) - d.exec_() - return + return error_dialog(self, _('Cannot save to disk'), + _('No books selected'), show=True) progress = ProgressDialog(_('Saving to disk...'), min=0, max=len(rows), parent=self) @@ -1266,8 +1282,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): config['show_text_in_toolbar'] else \ Qt.ToolButtonIconOnly) self.save_menu.actions()[2].setText( - _('Save only %s format to disk')%config.get( - 'save_to_disk_single_format').upper()) + _('Save only %s format to disk')% + prefs['output_format'].upper()) if self.library_path != d.database_location: try: newloc = d.database_location @@ -1414,8 +1430,9 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): return if isinstance(job.exception, JobKilled): return - error_dialog(self, _('Conversion Error'), job.gui_text(), - show=True) + error_dialog(self, _('Conversion Error'), + _('Failed to process')+': '+unicode(job.description), + det_msg=job.console_text()).exec_() def initialize_database(self): diff --git a/src/calibre/gui2/main.ui b/src/calibre/gui2/main.ui index 89bc2dfa79..5d0f29b56e 100644 --- a/src/calibre/gui2/main.ui +++ b/src/calibre/gui2/main.ui @@ -42,7 +42,7 @@ 16777215 - 100 + 75 diff --git a/src/calibre/gui2/main_window.py b/src/calibre/gui2/main_window.py index 903848fb41..b77a3fe7c8 100644 --- a/src/calibre/gui2/main_window.py +++ b/src/calibre/gui2/main_window.py @@ -7,6 +7,7 @@ from PyQt4.Qt import QMainWindow, QString, Qt, QFont, QCoreApplication, SIGNAL,\ QAction, QMenu, QMenuBar, QIcon from calibre.gui2.dialogs.conversion_error import ConversionErrorDialog from calibre.utils.config import OptionParser +from calibre.gui2 import error_dialog def option_parser(usage='''\ Usage: %prog [options] @@ -19,26 +20,26 @@ Launch the Graphical User Interface return parser class DebugWindow(ConversionErrorDialog): - + def __init__(self, parent): ConversionErrorDialog.__init__(self, parent, 'Console output', '') self.setModal(Qt.NonModal) font = QFont() font.setStyleHint(QFont.TypeWriter) self.text.setFont(font) - + def write(self, msg): self.text.setPlainText(self.text.toPlainText()+QString(msg)) - + def flush(self): - pass + pass class MainWindow(QMainWindow): ___menu_bar = None ___menu = None - __actions = [] - + __actions = [] + @classmethod def create_application_menubar(cls): mb = QMenuBar(None) @@ -50,8 +51,8 @@ class MainWindow(QMainWindow): mb.addMenu(menu) cls.___menu_bar = mb cls.___menu = menu - - + + @classmethod def get_menubar_actions(cls): preferences_action = QAction(QIcon(':/images/config.svg'), _('&Preferences'), None) @@ -59,7 +60,7 @@ class MainWindow(QMainWindow): preferences_action.setMenuRole(QAction.PreferencesRole) quit_action.setMenuRole(QAction.QuitRole) return preferences_action, quit_action - + def __init__(self, opts, parent=None): QMainWindow.__init__(self, parent) app = QCoreApplication.instance() @@ -69,19 +70,18 @@ class MainWindow(QMainWindow): self.__console_redirect = DebugWindow(self) sys.stdout = sys.stderr = self.__console_redirect self.__console_redirect.show() - + def unix_signal(self, signal): print 'Received signal:', repr(signal) - + def unhandled_exception(self, type, value, tb): try: sio = StringIO.StringIO() traceback.print_exception(type, value, tb, file=sio) fe = sio.getvalue() print >>sys.stderr, fe - msg = '

' + unicode(str(value), 'utf8', 'replace') + '

' - msg += '

Detailed traceback:

'+fe+'
' - d = ConversionErrorDialog(self, _('ERROR: Unhandled exception'), msg) - d.exec_() + msg = unicode(str(value), 'utf8', 'replace') + error_dialog(self, _('ERROR: Unhandled exception'), msg, det_msg=fe, + show=True) except: pass diff --git a/src/calibre/gui2/tools.py b/src/calibre/gui2/tools.py index b8dbbd5eba..4f3f837679 100644 --- a/src/calibre/gui2/tools.py +++ b/src/calibre/gui2/tools.py @@ -7,22 +7,22 @@ __docformat__ = 'restructuredtext en' Logic for setting up conversion jobs ''' -import cPickle, os +import cPickle from PyQt4.Qt import QDialog from calibre.ptempfile import PersistentTemporaryFile from calibre.gui2 import warning_dialog -from calibre.gui2.convert import load_specifics from calibre.gui2.convert.single import NoSupportedInputFormats from calibre.gui2.convert.single import Config as SingleConfig from calibre.gui2.convert.bulk import BulkConfig +from calibre.utils.config import prefs def convert_single_ebook(parent, db, book_ids, auto_conversion=False, out_format=None): changed = False jobs = [] bad = [] - + total = len(book_ids) if total == 0: return None, None, None @@ -33,23 +33,23 @@ def convert_single_ebook(parent, db, book_ids, auto_conversion=False, out_format try: d = SingleConfig(parent, db, book_id, None, out_format) - + if auto_conversion: d.accept() result = QDialog.Accepted else: result = d.exec_() - + if result == QDialog.Accepted: mi = db.get_metadata(book_id, True) in_file = db.format_abspath(book_id, d.input_format, True) - + out_file = PersistentTemporaryFile('.' + d.output_format) out_file.write(d.output_format) out_file.close() - + desc = _('Convert book %d of %d (%s)') % (i + 1, total, repr(mi.title)) - + recs = cPickle.loads(d.recommendations) args = [in_file, out_file.name, recs] temp_files = [out_file] @@ -76,7 +76,7 @@ def convert_bulk_ebook(parent, db, book_ids, out_format=None): changed = False jobs = [] bad = [] - + total = len(book_ids) if total == 0: return None, None, None @@ -95,16 +95,16 @@ def convert_bulk_ebook(parent, db, book_ids, out_format=None): try: d = SingleConfig(parent, db, book_id, None, output_format) d.accept() - + mi = db.get_metadata(book_id, True) in_file = db.format_abspath(book_id, d.input_format, True) - + out_file = PersistentTemporaryFile('.' + output_format) out_file.write(output_format) out_file.close() - + desc = _('Convert book %d of %d (%s)') % (i + 1, total, repr(mi.title)) - + args = [in_file, out_file.name, recs] temp_files = [out_file] jobs.append(('gui_convert', args, desc, d.output_format.upper(), book_id, temp_files)) @@ -126,33 +126,18 @@ def convert_bulk_ebook(parent, db, book_ids, out_format=None): return jobs, changed, bad -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 'fconvert_bulk_ebookseeds2'+fmt.lower(), [args], _('Fetch news from ')+data['title'], fmt.upper(), [pt] - - def fetch_scheduled_recipe(recipe, script): from calibre.gui2.dialogs.scheduler import config fmt = prefs['output_format'].lower() - pt = PersistentTemporaryFile(suffix='_feeds2%s.%s'%(fmt.lower(), fmt.lower())) + pt = PersistentTemporaryFile(suffix='_recipe_out.%s'%fmt.lower()) pt.close() - args = ['feeds2%s'%fmt.lower(), '--output', pt.name, '--debug'] + args = ['ebook-convert', script, pt.name, '-vv'] if recipe.needs_subscription: x = config.get('recipe_account_info_%s'%recipe.id, False) if not x: raise ValueError(_('You must set a username and password for %s')%recipe.title) args.extend(['--username', x[0], '--password', x[1]]) - args.append(script) - return 'feeds2'+fmt, [args], _('Fetch news from ')+recipe.title, fmt.upper(), [pt] -def fetch_news(data): - fmt = prefs['output_format'].lower() - return _fetch_news(data, fmt) + return 'ebook-convert', [args], _('Fetch news from ')+recipe.title, fmt.upper(), [pt] + diff --git a/src/calibre/gui2/viewer/main.py b/src/calibre/gui2/viewer/main.py index 0b8800035a..13553c6dc2 100644 --- a/src/calibre/gui2/viewer/main.py +++ b/src/calibre/gui2/viewer/main.py @@ -19,7 +19,6 @@ from calibre.gui2 import Application, ORG_NAME, APP_UID, choose_files, \ info_dialog, error_dialog from calibre.ebooks.oeb.iterator import EbookIterator from calibre.ebooks import DRMError -from calibre.gui2.dialogs.conversion_error import ConversionErrorDialog from calibre.constants import islinux from calibre.utils.config import Config, StringConfig from calibre.gui2.library import SearchBox @@ -543,8 +542,8 @@ class EbookViewer(MainWindow, Ui_EbookViewer): if isinstance(worker.exception, DRMError): error_dialog(self, _('DRM Error'), _('

This book is protected by DRM')%'http://wiki.mobileread.com/wiki/DRM').exec_() else: - ConversionErrorDialog(self, _('Could not open ebook'), - _('%s

%s

')%(worker.exception, worker.traceback.replace('\n', '
')), show=True) + error_dialog(self, _('Could not open ebook'), + unicode(worker.exception), det_msg=worker.traceback, show=True) self.close_progress_indicator() else: self.metadata.show_opf(self.iterator.opf) diff --git a/src/calibre/utils/config.py b/src/calibre/utils/config.py index f1f4a6a771..b3e1cd2cf9 100644 --- a/src/calibre/utils/config.py +++ b/src/calibre/utils/config.py @@ -14,7 +14,7 @@ from optparse import IndentedHelpFormatter from PyQt4.QtCore import QString from calibre.constants import terminal_controller, iswindows, isosx, \ __appname__, __version__, __author__, plugins -from calibre.utils.lock import LockError, ExclusiveFile +from calibre.utils.lock import LockError, ExclusiveFile from collections import defaultdict if os.environ.has_key('CALIBRE_CONFIG_DIRECTORY'): @@ -38,14 +38,14 @@ def make_config_dir(): class CustomHelpFormatter(IndentedHelpFormatter): - + def format_usage(self, usage): return _("%sUsage%s: %s\n") % (terminal_controller.BLUE, terminal_controller.NORMAL, usage) - + def format_heading(self, heading): - return "%*s%s%s%s:\n" % (self.current_indent, terminal_controller.BLUE, + return "%*s%s%s%s:\n" % (self.current_indent, terminal_controller.BLUE, "", heading, terminal_controller.NORMAL) - + def format_option(self, option): result = [] opts = self.option_strings[option] @@ -55,14 +55,14 @@ class CustomHelpFormatter(IndentedHelpFormatter): terminal_controller.GREEN+opts+terminal_controller.NORMAL) indent_first = self.help_position else: # start help on same line as opts - opts = "%*s%-*s " % (self.current_indent, "", opt_width + len(terminal_controller.GREEN + terminal_controller.NORMAL), + opts = "%*s%-*s " % (self.current_indent, "", opt_width + len(terminal_controller.GREEN + terminal_controller.NORMAL), terminal_controller.GREEN + opts + terminal_controller.NORMAL) indent_first = 0 result.append(opts) if option.help: help_text = self.expand_default(option).split('\n') help_lines = [] - + for line in help_text: help_lines.extend(textwrap.wrap(line, self.help_width)) result.append("%*s%s\n" % (indent_first, "", help_lines[0])) @@ -74,7 +74,7 @@ class CustomHelpFormatter(IndentedHelpFormatter): class OptionParser(_OptionParser): - + def __init__(self, usage='%prog [options] filename', version='%%prog (%s %s)'%(__appname__, __version__), @@ -85,16 +85,16 @@ class OptionParser(_OptionParser): usage = textwrap.dedent(usage) usage += '''\n\nWhenever you pass arguments to %prog that have spaces in them, '''\ '''enclose the arguments in quotation marks.''' - _OptionParser.__init__(self, usage=usage, version=version, epilog=epilog, - formatter=CustomHelpFormatter(), + _OptionParser.__init__(self, usage=usage, version=version, epilog=epilog, + formatter=CustomHelpFormatter(), conflict_handler=conflict_handler, **kwds) self.gui_mode = gui_mode - + def error(self, msg): if self.gui_mode: raise Exception(msg) _OptionParser.error(self, msg) - + def merge(self, parser): ''' Add options from parser to self. In case of conflicts, conflicting options from @@ -102,18 +102,18 @@ class OptionParser(_OptionParser): ''' opts = list(parser.option_list) groups = list(parser.option_groups) - + def merge_options(options, container): for opt in deepcopy(options): if not self.has_option(opt.get_opt_string()): container.add_option(opt) - + merge_options(opts, self) - + for group in groups: g = self.add_option_group(group.title) merge_options(group.option_list, g) - + def subsume(self, group_name, msg=''): ''' Move all existing options into a subgroup named @@ -125,7 +125,7 @@ class OptionParser(_OptionParser): for opt in opts: self.remove_option(opt.get_opt_string()) subgroup.add_option(opt) - + def options_iter(self): for opt in self.option_list: if str(opt).strip(): @@ -134,12 +134,12 @@ class OptionParser(_OptionParser): for opt in gr.option_list: if str(opt).strip(): yield opt - + def option_by_dest(self, dest): for opt in self.options_iter(): if opt.dest == dest: return opt - + def merge_options(self, lower, upper): ''' Merge options in lower and upper option lists into upper. @@ -153,16 +153,16 @@ class OptionParser(_OptionParser): if lower.__dict__[dest] != opt.default and \ upper.__dict__[dest] == opt.default: upper.__dict__[dest] = lower.__dict__[dest] - + class Option(object): - - def __init__(self, name, switches=[], help='', type=None, choices=None, + + def __init__(self, name, switches=[], help='', type=None, choices=None, check=None, group=None, default=None, action=None, metavar=None): if choices: type = 'choice' - + self.name = name self.switches = switches self.help = help.replace('%default', repr(default)) if help else None @@ -172,40 +172,40 @@ class Option(object): self.type = 'float' elif isinstance(default, int) and not isinstance(default, bool): self.type = 'int' - + self.choices = choices self.check = check self.group = group self.default = default self.action = action self.metavar = metavar - + def __eq__(self, other): return self.name == getattr(other, 'name', other) - + def __repr__(self): return 'Option: '+self.name - + def __str__(self): return repr(self) - + class OptionValues(object): - + def copy(self): return deepcopy(self) class OptionSet(object): - - OVERRIDE_PAT = re.compile(r'#{3,100} Override Options #{15}(.*?)#{3,100} End Override #{3,100}', + + OVERRIDE_PAT = re.compile(r'#{3,100} Override Options #{15}(.*?)#{3,100} End Override #{3,100}', re.DOTALL|re.IGNORECASE) - + def __init__(self, description=''): self.description = description self.preferences = [] self.group_list = [] self.groups = {} self.set_buffer = {} - + def has_option(self, name_or_option_object): if name_or_option_object in self.preferences: return True @@ -213,14 +213,14 @@ class OptionSet(object): if p.name == name_or_option_object: return True return False - + def add_group(self, name, description=''): if name in self.group_list: raise ValueError('A group by the name %s already exists in this set'%name) self.groups[name] = description self.group_list.append(name) return partial(self.add_opt, group=name) - + def update(self, other): for name in other.groups.keys(): self.groups[name] = other.groups[name] @@ -230,7 +230,7 @@ class OptionSet(object): if pref in self.preferences: self.preferences.remove(pref) self.preferences.append(pref) - + def smart_update(self, opts1, opts2): ''' Updates the preference values in opts1 using only the non-default preference values in opts2. @@ -239,47 +239,47 @@ class OptionSet(object): new = getattr(opts2, pref.name, pref.default) if new != pref.default: setattr(opts1, pref.name, new) - + def remove_opt(self, name): if name in self.preferences: self.preferences.remove(name) - - - def add_opt(self, name, switches=[], help=None, type=None, choices=None, + + + def add_opt(self, name, switches=[], help=None, type=None, choices=None, group=None, default=None, action=None, metavar=None): ''' Add an option to this section. - + :param name: The name of this option. Must be a valid Python identifier. - Must also be unique in this OptionSet and all its subsets. - :param switches: List of command line switches for this option + Must also be unique in this OptionSet and all its subsets. + :param switches: List of command line switches for this option (as supplied to :module:`optparse`). If empty, this option will not be added to the command line parser. :param help: Help text. :param type: Type checking of option values. Supported types are: `None, 'choice', 'complex', 'float', 'int', 'string'`. :param choices: List of strings or `None`. - :param group: Group this option belongs to. You must previously + :param group: Group this option belongs to. You must previously have created this group with a call to :method:`add_group`. :param default: The default value for this option. :param action: The action to pass to optparse. Supported values are: `None, 'count'`. For choices and boolean options, action is automatically set correctly. ''' - pref = Option(name, switches=switches, help=help, type=type, choices=choices, + pref = Option(name, switches=switches, help=help, type=type, choices=choices, group=group, default=default, action=action, metavar=None) if group is not None and group not in self.groups.keys(): raise ValueError('Group %s has not been added to this section'%group) if pref in self.preferences: raise ValueError('An option with the name %s already exists in this set.'%name) self.preferences.append(pref) - + def option_parser(self, user_defaults=None, usage='', gui_mode=False): parser = OptionParser(usage, gui_mode=gui_mode) groups = defaultdict(lambda : parser) for group, desc in self.groups.items(): groups[group] = parser.add_option_group(group.upper(), desc) - + for pref in self.preferences: if not pref.switches: continue @@ -299,16 +299,16 @@ class OptionSet(object): action=action, ) g.add_option(*pref.switches, **args) - - + + return parser - + def get_override_section(self, src): match = self.OVERRIDE_PAT.search(src) if match: return match.group() return '' - + def parse_string(self, src): options = {'cPickle':cPickle} if not isinstance(src, unicode): @@ -327,9 +327,9 @@ class OptionSet(object): if callable(formatter): val = formatter(val) setattr(opts, pref.name, val) - + return opts - + def render_group(self, name, desc, opts): prefs = [pref for pref in self.preferences if pref.group == name] lines = ['### Begin group: %s'%(name if name else 'DEFAULT')] @@ -340,11 +340,11 @@ class OptionSet(object): lines.append('# '+pref.name.replace('_', ' ')) if pref.help: lines += map(lambda x: '# ' + x, pref.help.split('\n')) - lines.append('%s = %s'%(pref.name, + lines.append('%s = %s'%(pref.name, self.serialize_opt(getattr(opts, pref.name, pref.default)))) lines.append(' ') return '\n'.join(lines) - + def serialize_opt(self, val): if val is val is True or val is False or val is None or \ isinstance(val, (int, float, long, basestring)): @@ -353,7 +353,7 @@ class OptionSet(object): return repr(unicode(val)) pickle = cPickle.dumps(val, -1) return 'cPickle.loads(%s)'%repr(pickle) - + def serialize(self, opts): src = '# %s\n\n'%(self.description.replace('\n', '\n# ')) groups = [self.render_group(name, self.groups.get(name, ''), opts) \ @@ -361,34 +361,34 @@ class OptionSet(object): return src + '\n\n'.join(groups) class ConfigInterface(object): - + def __init__(self, description): self.option_set = OptionSet(description=description) self.add_opt = self.option_set.add_opt self.add_group = self.option_set.add_group self.remove_opt = self.remove = self.option_set.remove_opt self.parse_string = self.option_set.parse_string - + def update(self, other): self.option_set.update(other.option_set) - + def option_parser(self, usage='', gui_mode=False): - return self.option_set.option_parser(user_defaults=self.parse(), + return self.option_set.option_parser(user_defaults=self.parse(), usage=usage, gui_mode=gui_mode) - + def smart_update(self, opts1, opts2): self.option_set.smart_update(opts1, opts2) - + class Config(ConfigInterface): ''' A file based configuration. ''' - + def __init__(self, basename, description=''): ConfigInterface.__init__(self, description) self.config_file_path = os.path.join(config_dir, basename+'.py') - - + + def parse(self): src = '' if os.path.exists(self.config_file_path): @@ -398,7 +398,7 @@ class Config(ConfigInterface): except LockError: raise IOError('Could not lock config file: %s'%self.config_file_path) return self.option_set.parse_string(src) - + def as_string(self): if not os.path.exists(self.config_file_path): return '' @@ -407,7 +407,7 @@ class Config(ConfigInterface): return f.read().decode('utf-8') except LockError: raise IOError('Could not lock config file: %s'%self.config_file_path) - + def set(self, name, val): if not self.option_set.has_option(name): raise ValueError('The option %s is not defined.'%name) @@ -427,19 +427,19 @@ class Config(ConfigInterface): f.write(src) except LockError: raise IOError('Could not lock config file: %s'%self.config_file_path) - + class StringConfig(ConfigInterface): ''' A string based configuration ''' - + def __init__(self, src, description=''): ConfigInterface.__init__(self, description) self.src = src - + def parse(self): return self.option_set.parse_string(self.src) - + def set(self, name, val): if not self.option_set.has_option(name): raise ValueError('The option %s is not defined.'%name) @@ -452,30 +452,30 @@ class ConfigProxy(object): ''' A Proxy to minimize file reads for widely used config settings ''' - + def __init__(self, config): self.__config = config - self.__opts = None - + self.__opts = None + def refresh(self): self.__opts = self.__config.parse() - + def __getitem__(self, key): return self.get(key) - + def __setitem__(self, key, val): return self.set(key, val) - + def get(self, key): if self.__opts is None: self.refresh() return getattr(self.__opts, key) - + def set(self, key, val): if self.__opts is None: self.refresh() setattr(self.__opts, key, val) - return self.__config.set(key, val) + return self.__config.set(key, val) class DynamicConfig(dict): ''' @@ -489,7 +489,7 @@ class DynamicConfig(dict): self.name = name self.file_path = os.path.join(config_dir, name+'.pickle') self.refresh() - + def refresh(self): d = {} if os.path.exists(self.file_path): @@ -503,20 +503,20 @@ class DynamicConfig(dict): d = {} self.clear() self.update(d) - + def __getitem__(self, key): try: return dict.__getitem__(self, key) except KeyError: return None - + def __setitem__(self, key, val): dict.__setitem__(self, key, val) self.commit() - + def set(self, key, val): self.__setitem__(key, val) - + def commit(self): if hasattr(self, 'file_path') and self.file_path: if not os.path.exists(self.file_path): @@ -526,17 +526,17 @@ class DynamicConfig(dict): f.seek(0) f.truncate() f.write(raw) - -dynamic = DynamicConfig() + +dynamic = DynamicConfig() def _prefs(): c = Config('global', 'calibre wide preferences') - c.add_opt('database_path', + c.add_opt('database_path', default=os.path.expanduser('~/library1.db'), help=_('Path to the database in which books are stored')) c.add_opt('filename_pattern', default=ur'(?P.+) - (?P<author>[^_]+)', help=_('Pattern to guess metadata from filenames')) - c.add_opt('isbndb_com_key', default='', + c.add_opt('isbndb_com_key', default='', help=_('Access key for isbndb.com')) c.add_opt('network_timeout', default=5, help=_('Default timeout for network operations (seconds)')) @@ -544,13 +544,13 @@ 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='EPUB', + c.add_opt('output_format', default='EPUB', help=_('The default output format for ebook conversions.')) c.add_opt('read_file_metadata', default=True, help=_('Read metadata from files')) - c.add_opt('worker_process_priority', default='normal', + c.add_opt('worker_process_priority', default='normal', help=_('The priority of worker processes')) - + c.add_opt('migrated', default=False, help='For Internal use. Don\'t modify.') return c @@ -565,11 +565,11 @@ def migrate(): from PyQt4.QtCore import QSettings, QVariant class Settings(QSettings): - + def __init__(self, name='calibre2'): QSettings.__init__(self, QSettings.IniFormat, QSettings.UserScope, 'kovidgoyal.net', name) - + def get(self, key, default=None): try: key = str(key) @@ -581,7 +581,7 @@ def migrate(): return cPickle.loads(val) except: return default - + s, migrated = Settings(), set([]) all_keys = set(map(unicode, s.allKeys())) from calibre.gui2 import config, dynamic @@ -599,13 +599,13 @@ def migrate(): pass finally: migrated.add(key) - - + + _migrate('database path', p=prefs) _migrate('filename pattern', p=prefs) _migrate('network timeout', p=prefs) _migrate('isbndb.com key', p=prefs) - + _migrate('frequently used directories') _migrate('send to device by default') _migrate('save to disk single format') @@ -616,33 +616,33 @@ def migrate(): _migrate('cover flow queue length') _migrate('LRF conversion defaults') _migrate('LRF ebook viewer options') - + for key in all_keys - migrated: if key.endswith(': un') or key.endswith(': pw'): _migrate(key, p=dynamic) p.set('migrated', True) - - + + if __name__ == '__main__': import subprocess from PyQt4.Qt import QByteArray c = Config('test', 'test config') - + c.add_opt('one', ['-1', '--one'], help="This is option #1") c.set('one', u'345') - + c.add_opt('two', help="This is option #2") c.set('two', 345) - + c.add_opt('three', help="This is option #3") c.set('three', QString(u'aflatoon')) - + c.add_opt('four', help="This is option #4") c.set('four', QByteArray('binary aflatoon')) - + subprocess.call(['pygmentize', os.path.expanduser('~/.config/calibre/test.py')]) - + opts = c.parse() for i in ('one', 'two', 'three', 'four'): print i, repr(getattr(opts, i)) - + diff --git a/src/calibre/web/feeds/input.py b/src/calibre/web/feeds/input.py index e0a8b807c8..ee003be0da 100644 --- a/src/calibre/web/feeds/input.py +++ b/src/calibre/web/feeds/input.py @@ -18,9 +18,13 @@ class RecipeInput(InputFormatPlugin): file_types = set(['recipe']) recommendations = set([ - ('chapter_mark', 'none', OptionRecommendation.HIGH), + ('chapter', None, OptionRecommendation.HIGH), ('dont_split_on_page_breaks', True, OptionRecommendation.HIGH), ('use_auto_toc', False, OptionRecommendation.HIGH), + ('input_encoding', None, OptionRecommendation.HIGH), + ('input_profile', 'default', OptionRecommendation.HIGH), + ('page_breaks_before', None, OptionRecommendation.HIGH), + ('insert_metadata', False, OptionRecommendation.HIGH), ]) options = set([ diff --git a/src/calibre/web/feeds/news.py b/src/calibre/web/feeds/news.py index b7b0d688d6..3f6b9b9ae1 100644 --- a/src/calibre/web/feeds/news.py +++ b/src/calibre/web/feeds/news.py @@ -231,23 +231,23 @@ class BasicNewsRecipe(Recipe): #: use :member:`extra_css` in your recipe to customize look and feel. template_css = u''' .article_date { - font-size: x-small; color: gray; font-family: monospace; + color: gray; font-family: monospace; } .article_description { - font-size: small; font-family: sans; text-indent: 0pt; + font-family: sans; text-indent: 0pt; } a.article { - font-weight: bold; font-size: large; + font-weight: bold; } a.feed { - font-weight: bold; font-size: large; + font-weight: bold; } .navbar { - font-family:monospace; font-size:8pt + font-family:monospace; } ''' @@ -579,8 +579,9 @@ class BasicNewsRecipe(Recipe): def feeds2index(self, feeds): templ = templates.IndexTemplate() + css = self.template_css + '\n\n' +(self.extra_css if self.extra_css else '') return templ.generate(self.title, self.timefmt, feeds, - extra_css=self.extra_css).render(doctype='xhtml') + extra_css=css).render(doctype='xhtml') @classmethod def description_limiter(cls, src): @@ -631,8 +632,9 @@ class BasicNewsRecipe(Recipe): templ = templates.FeedTemplate() + css = self.template_css + '\n\n' +(self.extra_css if self.extra_css else '') return templ.generate(feed, self.description_limiter, - extra_css=self.extra_css).render(doctype='xhtml') + extra_css=css).render(doctype='xhtml') def _fetch_article(self, url, dir, f, a, num_of_feeds): diff --git a/src/calibre/web/feeds/recipes/recipe_dna.py b/src/calibre/web/feeds/recipes/recipe_dna.py index 6ec9ba4665..a335fd5655 100644 --- a/src/calibre/web/feeds/recipes/recipe_dna.py +++ b/src/calibre/web/feeds/recipes/recipe_dna.py @@ -5,12 +5,13 @@ import re from calibre.web.feeds.news import BasicNewsRecipe class DNAIndia(BasicNewsRecipe): - + title = 'DNA India' description = 'Mumbai news, India news, World news, breaking news' __author__ = 'Kovid Goyal' language = _('English') - + encoding = 'cp1252' + feeds = [ ('Top News', 'http://www.dnaindia.com/syndication/rss_topnews.xml'), ('Popular News', 'http://www.dnaindia.com/syndication/rss_popular.xml'), @@ -21,21 +22,21 @@ class DNAIndia(BasicNewsRecipe): ('Money', 'http://www.dnaindia.com/syndication/rss,catid-4.xml'), ('Sports', 'http://www.dnaindia.com/syndication/rss,catid-6.xml'), ('After Hours', 'http://www.dnaindia.com/syndication/rss,catid-7.xml'), - ('Digital Life', 'http://www.dnaindia.com/syndication/rss,catid-1089741.xml'), + ('Digital Life', 'http://www.dnaindia.com/syndication/rss,catid-1089741.xml'), ] remove_tags = [{'id':'footer'}, {'class':['bottom', 'categoryHead']}] - + def print_version(self, url): match = re.search(r'newsid=(\d+)', url) if not match: return url return 'http://www.dnaindia.com/dnaprint.asp?newsid='+match.group(1) - + def postprocess_html(self, soup, first_fetch): for t in soup.findAll(['table', 'tr', 'td']): t.name = 'div' - + a = soup.find(href='http://www.3dsyndication.com/') if a is not None: a.parent.extract() - return soup \ No newline at end of file + return soup diff --git a/src/calibre/web/feeds/recipes/recipe_newsweek.py b/src/calibre/web/feeds/recipes/recipe_newsweek.py index 6f47019f46..3197989da8 100644 --- a/src/calibre/web/feeds/recipes/recipe_newsweek.py +++ b/src/calibre/web/feeds/recipes/recipe_newsweek.py @@ -5,7 +5,6 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' import re, string, time from calibre import strftime from calibre.web.feeds.news import BasicNewsRecipe -from calibre.ebooks.BeautifulSoup import BeautifulSoup class Newsweek(BasicNewsRecipe): @@ -14,15 +13,20 @@ class Newsweek(BasicNewsRecipe): description = 'Weekly news and current affairs in the US' no_stylesheets = True language = _('English') - - extra_css = '#content { font:serif 12pt; }\n.story {font:12pt}\n.HorizontalHeader {font:18pt}\n.deck {font:16pt}' + + extra_css = ''' + #content { font-size:normal; font-family: serif } + .story { font-size:normal } + .HorizontalHeader {font-size:xx-large} + .deck {font-size:x-large} + ''' keep_only_tags = [dict(name='div', id='content')] remove_tags = [ dict(name=['script', 'noscript']), dict(name='div', attrs={'class':['ad', 'SocialLinks', 'SocialLinksDiv', - 'channel', 'bot', 'nav', 'top', - 'EmailArticleBlock', + 'channel', 'bot', 'nav', 'top', + 'EmailArticleBlock', 'comments-and-social-links-wrapper', 'inline-social-links-wrapper', 'inline-social-links', @@ -31,14 +35,14 @@ class Newsweek(BasicNewsRecipe): dict(id=['ToolBox', 'EmailMain', 'EmailArticle', 'comment-box', 'nw-comments']) ] - + recursions = 1 match_regexps = [r'http://www.newsweek.com/id/\S+/page/\d+'] - - + + def get_sections(self, soup): sections = [] - + def process_section(img): articles = [] match = re.search(r'label_([^_.]+)', img['src']) @@ -48,25 +52,25 @@ class Newsweek(BasicNewsRecipe): if title in ['coverstory', 'more', 'tipsheet']: return title = string.capwords(title) - + for a in img.parent.findAll('a', href=True): art, href = a.string, a['href'] if not re.search('\d+$', href) or not art or 'Preview Article' in art: continue articles.append({ - 'title':art, 'url':href, 'description':'', - 'content':'', 'date':'' + 'title':art, 'url':href, 'description':'', + 'content':'', 'date':'' }) sections.append((title, articles)) - + img.parent.extract() for img in soup.findAll(src=re.compile('/label_')): process_section(img) - + return sections - + def parse_index(self): soup = self.get_current_issue() if not soup: @@ -78,10 +82,10 @@ class Newsweek(BasicNewsRecipe): if match is not None: self.timefmt = strftime(' [%d %b, %Y]', time.strptime(match.group(1), '%y%m%d')) self.cover_url = small.replace('coversmall', 'coverlarge') - + sections = self.get_sections(soup) sections.insert(0, ('Main articles', [])) - + for tag in soup.findAll('h5'): a = tag.find('a', href=True) if a is not None: @@ -97,8 +101,8 @@ class Newsweek(BasicNewsRecipe): if art['title'] and art['url']: sections[0][1].append(art) return sections - - + + def postprocess_html(self, soup, first_fetch): divs = list(soup.findAll('div', 'pagination')) if not divs: @@ -106,8 +110,8 @@ class Newsweek(BasicNewsRecipe): divs[0].extract() if len(divs) > 1: soup.find('body')['style'] = 'page-break-after:avoid' - divs[1].extract() - + divs[1].extract() + h1 = soup.find('h1') if h1: h1.extract() @@ -116,12 +120,12 @@ class Newsweek(BasicNewsRecipe): else: soup.find('body')['style'] = 'page-break-before:always; page-break-after:avoid;' return soup - + def get_current_issue(self): #from urllib2 import urlopen # For some reason mechanize fails - #home = urlopen('http://www.newsweek.com').read() + #home = urlopen('http://www.newsweek.com').read() soup = self.index_to_soup('http://www.newsweek.com')#BeautifulSoup(home) img = soup.find('img', alt='Current Magazine') if img and img.parent.has_key('href'): return self.index_to_soup(img.parent['href']) - + diff --git a/src/calibre/web/feeds/templates.py b/src/calibre/web/feeds/templates.py index 1a6a574129..ede6ec821c 100644 --- a/src/calibre/web/feeds/templates.py +++ b/src/calibre/web/feeds/templates.py @@ -7,7 +7,7 @@ from calibre import preferred_encoding, strftime class Template(MarkupTemplate): - + def generate(self, *args, **kwargs): if not kwargs.has_key('style'): kwargs['style'] = '' @@ -17,20 +17,20 @@ class Template(MarkupTemplate): for arg in args: if isinstance(arg, basestring) and not isinstance(arg, unicode): arg = unicode(arg, 'utf-8', 'replace') - + return MarkupTemplate.generate(self, *args, **kwargs) - + class NavBarTemplate(Template): - + def __init__(self): Template.__init__(self, u'''\ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" xmlns:xi="http://www.w3.org/2001/XInclude" - xmlns:py="http://genshi.edgewall.org/" - + xmlns:py="http://genshi.edgewall.org/" + > <head> <style py:if="extra_css" type="text/css"> @@ -38,7 +38,7 @@ class NavBarTemplate(Template): </style> </head> <body> - <div class="navbar" style="text-align:${'center' if center else 'left'};"> + <div class="navbar calibre_rescale_70" style="text-align:${'center' if center else 'left'};"> <hr py:if="bottom" /> <p py:if="bottom" style="text-align:left"> This article was downloaded by <b>${__appname__}</b> from <a href="${url}">${url}</a> @@ -50,7 +50,7 @@ class NavBarTemplate(Template): <py:if test="art == num - 1 and not bottom"> | <a href="${prefix}../../feed_${str(feed+1)}/index.html">Next</a> </py:if> - | <a href="${prefix}../index.html#article_${str(art)}">Section menu</a> + | <a href="${prefix}../index.html#article_${str(art)}">Section menu</a> <py:if test="two_levels"> | <a href="${prefix}../../index.html#feed_${str(feed)}">Main menu</a> </py:if> @@ -64,29 +64,29 @@ class NavBarTemplate(Template): </html> ''') - def generate(self, bottom, feed, art, number_of_articles_in_feed, + def generate(self, bottom, feed, art, number_of_articles_in_feed, two_levels, url, __appname__, prefix='', center=True, extra_css=None): if prefix and not prefix.endswith('/'): prefix += '/' return Template.generate(self, bottom=bottom, art=art, feed=feed, - num=number_of_articles_in_feed, + num=number_of_articles_in_feed, two_levels=two_levels, url=url, __appname__=__appname__, prefix=prefix, center=center, extra_css=extra_css) - + class IndexTemplate(Template): - + def __init__(self): Template.__init__(self, u'''\ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" xmlns:xi="http://www.w3.org/2001/XInclude" - xmlns:py="http://genshi.edgewall.org/" - + xmlns:py="http://genshi.edgewall.org/" + > <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> @@ -99,15 +99,17 @@ class IndexTemplate(Template): </style> </head> <body> - <h1 class="calibre_recipe_title">${title}</h1> - <p style="text-align:right">${date}</p> - <ul class="calibre_feed_list"> - <py:for each="i, feed in enumerate(feeds)"> - <li py:if="feed" id="feed_${str(i)}"> - <a class="feed" href="${'feed_%d/index.html'%i}">${feed.title}</a> - </li> - </py:for> - </ul> + <div class="calibre_rescale_100"> + <h1 class="calibre_recipe_title calibre_rescale_180">${title}</h1> + <p style="text-align:right">${date}</p> + <ul class="calibre_feed_list"> + <py:for each="i, feed in enumerate(feeds)"> + <li py:if="feed" id="feed_${str(i)}"> + <a class="feed calibre_rescale_120" href="${'feed_%d/index.html'%i}">${feed.title}</a> + </li> + </py:for> + </ul> + </div> </body> </html> ''') @@ -118,19 +120,19 @@ class IndexTemplate(Template): date = strftime(datefmt) return Template.generate(self, title=title, date=date, feeds=feeds, extra_css=extra_css) - - + + class FeedTemplate(Template): - + def __init__(self): Template.__init__(self, u'''\ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" xmlns:xi="http://www.w3.org/2001/XInclude" - xmlns:py="http://genshi.edgewall.org/" - + xmlns:py="http://genshi.edgewall.org/" + > <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> @@ -143,61 +145,64 @@ class FeedTemplate(Template): </style> </head> <body style="page-break-before:always"> - <h2 class="calibre_feed_title">${feed.title}</h2> + <div class="calibre_rescale_100"> + <h2 class="calibre_feed_title calibre_rescale_160">${feed.title}</h2> <py:if test="getattr(feed, 'image', None)"> <div class="calibre_feed_image"> <img alt="${feed.image_alt}" src="${feed.image_url}" /> </div> </py:if> - <div class="calibre_feed_description" py:if="getattr(feed, 'description', None)"> + <div class="calibre_feed_description calibre_rescale_80" py:if="getattr(feed, 'description', None)"> ${feed.description}<br /> </div> <ul class="calibre_article_list"> <py:for each="i, article in enumerate(feed.articles)"> - <li id="${'article_%d'%i}" py:if="getattr(article, 'downloaded', False)" style="padding-bottom:0.5em"> - <a class="article" href="${article.url}">${article.title}</a> + <li id="${'article_%d'%i}" py:if="getattr(article, 'downloaded', + False)" style="padding-bottom:0.5em" class="calibre_rescale_100"> + <a class="article calibre_rescale_120" href="${article.url}">${article.title}</a> <span class="article_date">${article.localtime.strftime(" [%a, %d %b %H:%M]")}</span> - <div class="article_decription" py:if="article.summary"> + <div class="article_decription calibre_rescale_70" py:if="article.summary"> ${Markup(cutoff(article.text_summary))} </div> </li> </py:for> </ul> - <div class="navbar" style="text-align:center; font-family:monospace; font-size:8pt"> + <div class="navbar calibre_rescale_70"> | <a href="../index.html">Up one level</a> | </div> + </div> </body> </html> ''') - + def generate(self, feed, cutoff, extra_css=None): - return Template.generate(self, feed=feed, cutoff=cutoff, + return Template.generate(self, feed=feed, cutoff=cutoff, extra_css=extra_css) class EmbeddedContent(Template): - + def __init__(self): Template.__init__(self, u'''\ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" xmlns:xi="http://www.w3.org/2001/XInclude" - xmlns:py="http://genshi.edgewall.org/" - + xmlns:py="http://genshi.edgewall.org/" + > <head> <title>${article.title} - +

${article.title}

${Markup(article.content if len(article.content if article.content else '') > len(article.summary if article.summary else '') else article.summary)}
- + ''') - + def generate(self, article): return Template.generate(self, article=article) diff --git a/src/calibre/web/fetch/simple.py b/src/calibre/web/fetch/simple.py index cbe048a011..0920161316 100644 --- a/src/calibre/web/fetch/simple.py +++ b/src/calibre/web/fetch/simple.py @@ -53,9 +53,15 @@ def save_soup(soup, target): ns = BeautifulSoup('') nm = ns.find('meta') metas = soup.findAll('meta', content=True) + added = False for meta in metas: if 'charset' in meta.get('content', '').lower(): meta.replaceWith(nm) + added = True + if not added: + head = soup.find('head') + if head is not None: + head.insert(0, nm) selfdir = os.path.dirname(target) @@ -67,6 +73,7 @@ def save_soup(soup, target): html = unicode(soup) with open(target, 'wb') as f: + idx = html.find('hoping') f.write(html.encode('utf-8')) class response(str):