Sycn to pluginize

This commit is contained in:
John Schember 2009-05-09 07:45:16 -04:00
commit d69b1ffbdc
23 changed files with 407 additions and 338 deletions

View File

@ -59,10 +59,9 @@ class HTMLRenderer(object):
def render_html(path_to_html, width=590, height=750): def render_html(path_to_html, width=590, height=750):
from PyQt4.QtWebKit import QWebPage from PyQt4.QtWebKit import QWebPage
from PyQt4.Qt import QEventLoop, QPalette, Qt, SIGNAL, QUrl, QSize, \ from PyQt4.Qt import QEventLoop, QPalette, Qt, SIGNAL, QUrl, QSize
QApplication from calibre.gui2 import is_ok_to_use_qt
if QApplication.instance() is None: if not is_ok_to_use_qt(): return None
QApplication([])
path_to_html = os.path.abspath(path_to_html) path_to_html = os.path.abspath(path_to_html)
with CurrentDir(os.path.dirname(path_to_html)): with CurrentDir(os.path.dirname(path_to_html)):
page = QWebPage() page = QWebPage()

View File

@ -80,8 +80,10 @@ class EPUBInput(InputFormatPlugin):
t.set('href', guide_cover) t.set('href', guide_cover)
t.set('title', 'Title Page') t.set('title', 'Title Page')
from calibre.ebooks import render_html from calibre.ebooks import render_html
open('calibre_raster_cover.jpg', 'wb').write( renderer = render_html(guide_cover)
render_html(guide_cover).data) if renderer is not None:
open('calibre_raster_cover.jpg', 'wb').write(
renderer.data)
def convert(self, stream, options, file_ext, log, accelerators): def convert(self, stream, options, file_ext, log, accelerators):

View File

@ -290,14 +290,6 @@ class MobiReader(object):
self.replace_page_breaks() self.replace_page_breaks()
self.cleanup_html() self.cleanup_html()
if self.processed_html.startswith('<body'):
self.processed_html = '<html><head></head>'+self.processed_html+'</html>'
self.processed_html = \
re.compile('<head>', re.IGNORECASE).sub(
'\n<head>\n'
'\t<link type="text/css" href="styles.css" />\n',
self.processed_html)
self.log.debug('Parsing HTML...') self.log.debug('Parsing HTML...')
root = html.fromstring(self.processed_html) root = html.fromstring(self.processed_html)
if root.xpath('descendant::p/descendant::p'): if root.xpath('descendant::p/descendant::p'):
@ -305,7 +297,7 @@ class MobiReader(object):
self.log.warning('Markup contains unclosed <p> tags, parsing using', self.log.warning('Markup contains unclosed <p> tags, parsing using',
'BeatifulSoup') 'BeatifulSoup')
root = soupparser.fromstring(self.processed_html) root = soupparser.fromstring(self.processed_html)
if root[0].tag != 'html': if root.tag != 'html':
self.log.warn('File does not have opening <html> tag') self.log.warn('File does not have opening <html> tag')
nroot = html.fromstring('<html><head></head><body></body></html>') nroot = html.fromstring('<html><head></head><body></body></html>')
bod = nroot.find('body') bod = nroot.find('body')
@ -314,6 +306,35 @@ class MobiReader(object):
bod.append(child) bod.append(child)
root = nroot root = nroot
htmls = list(root.xpath('//html'))
if len(htmls) > 1:
self.log.warn('Markup contains multiple <html> 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) self.upshift_markup(root)
guides = root.xpath('//guide') guides = root.xpath('//guide')
guide = guides[0] if guides else None guide = guides[0] if guides else None

View File

@ -24,6 +24,19 @@ def asfloat(value, default):
value = default value = default
return float(value) 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): class KeyMapper(object):
def __init__(self, sbase, dbase, dkey): def __init__(self, sbase, dbase, dkey):
@ -202,11 +215,19 @@ class CSSFlattener(object):
if 'bgcolor' in node.attrib: if 'bgcolor' in node.attrib:
cssdict['background-color'] = node.attrib['bgcolor'] cssdict['background-color'] = node.attrib['bgcolor']
del node.attrib['bgcolor'] del node.attrib['bgcolor']
if not self.context.disable_font_rescaling and \ if not self.context.disable_font_rescaling:
'font-size' in cssdict or tag == 'body': _sbase = self.sbase if self.sbase is not None else \
fsize = self.fmap[style['font-size']] self.context.source.fbase
cssdict['font-size'] = "%0.5fem" % (fsize / psize) dyn_rescale = dynamic_rescale_factor(node)
psize = fsize 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 cssdict:
if self.lineh and self.fbase and tag != 'body': if self.lineh and self.fbase and tag != 'body':
self.clean_edges(cssdict, style, psize) self.clean_edges(cssdict, style, psize)

View File

@ -25,14 +25,16 @@ class Jacket(object):
<title>%(title)s</title> <title>%(title)s</title>
</head> </head>
<body> <body>
<div style="text-align:center"> <div class="calibre_rescale_100">
<h1>%(title)s</h1> <div style="text-align:center">
<h2>%(jacket)s</h2> <h1 class="calibre_rescale_180">%(title)s</h1>
<div>%(series)s</div> <h2 class="calibre_rescale_140">%(jacket)s</h2>
<div>%(tags)s</div> <div class="calibre_rescale_100">%(series)s</div>
</div> <div class="calibre_rescale_100">%(tags)s</div>
<div style="margin-top:2em"> </div>
%(comments)s <div style="margin-top:2em" class="calibre_rescale_100">
%(comments)s
</div>
</div> </div>
</body> </body>
</html> </html>

View File

@ -369,13 +369,13 @@ class FlowSplitter(object):
for path in ( for path in (
'//*[re:match(name(), "h[1-6]", "i")]', '//*[re:match(name(), "h[1-6]", "i")]',
'/html/body/div', '/h:html/h:body/h:div',
'//pre', '//h:pre',
'//hr', '//h:hr',
'//p', '//h:p',
'//div', '//h:div',
'//br', '//h:br',
'//li', '//h:li',
): ):
elems = root.xpath(path, namespaces=NAMESPACES) elems = root.xpath(path, namespaces=NAMESPACES)
elem = pick_elem(elems) elem = pick_elem(elems)

View File

@ -27,8 +27,6 @@ def _config():
help=_('Frequently used directories')) help=_('Frequently used directories'))
c.add_opt('send_to_storage_card_by_default', default=False, c.add_opt('send_to_storage_card_by_default', default=False,
help=_('Send file to storage card instead of main memory by default')) 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, c.add_opt('confirm_delete', default=False,
help=_('Confirm before deleting')) help=_('Confirm before deleting'))
c.add_opt('toolbar_icon_size', default=QSize(48, 48), c.add_opt('toolbar_icon_size', default=QSize(48, 48),
@ -100,18 +98,23 @@ def available_width():
def extension(path): def extension(path):
return os.path.splitext(path)[1][1:].lower() 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, d = QMessageBox(QMessageBox.Warning, 'WARNING: '+title, msg, QMessageBox.Ok,
parent) parent)
d.setDetailedText(det_msg) d.setDetailedText(det_msg)
d.setIconPixmap(QPixmap(':/images/dialog_warning.svg')) d.setIconPixmap(QPixmap(':/images/dialog_warning.svg'))
if show:
return d.exec_()
return d 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, d = QMessageBox(QMessageBox.Critical, 'ERROR: '+title, msg, QMessageBox.Ok,
parent) parent)
d.setDetailedText(det_msg) d.setDetailedText(det_msg)
d.setIconPixmap(QPixmap(':/images/dialog_error.svg')) d.setIconPixmap(QPixmap(':/images/dialog_error.svg'))
if show:
return d.exec_()
return d return d
def question_dialog(parent, title, msg, det_msg=''): 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')) d.setIconPixmap(QPixmap(':/images/dialog_information.svg'))
return d 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, d = QMessageBox(QMessageBox.Information, title, msg, QMessageBox.NoButton,
parent) parent)
d.setDetailedText(det_msg) d.setDetailedText(det_msg)
d.setIconPixmap(QPixmap(':/images/dialog_information.svg')) d.setIconPixmap(QPixmap(':/images/dialog_information.svg'))
if show:
return d.exec_()
return d return d
def qstring_to_unicode(q): def qstring_to_unicode(q):
return unicode(q) return unicode(q)

View File

@ -530,8 +530,8 @@ class DeviceGUI(object):
]) ])
error_dialog(self, _('Failed to email books'), error_dialog(self, _('Failed to email books'),
_('Failed to email the following books:'), _('Failed to email the following books:'),
'%s'%errors, '%s'%errors
show=True) )
else: else:
self.status_bar.showMessage(_('Sent by email:') + ', '.join(good), self.status_bar.showMessage(_('Sent by email:') + ', '.join(good),
5000) 5000)

View File

@ -399,12 +399,15 @@ class ConfigDialog(QDialog, Ui_Dialog):
default_index = self.output_format.findText(prefs['output_format']) default_index = self.output_format.findText(prefs['output_format'])
self.output_format.setCurrentIndex(default_index if default_index != -1 else 0) self.output_format.setCurrentIndex(default_index if default_index != -1 else 0)
self.book_exts = sorted(BOOK_EXTENSIONS) output_formats = sorted(available_output_formats())
for ext in self.book_exts: output_formats.remove('oeb')
self.single_format.addItem(ext.upper(), QVariant(ext)) 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.cover_browse.setValue(config['cover_flow_queue_length'])
self.systray_notifications.setChecked(not config['disable_tray_notification']) self.systray_notifications.setChecked(not config['disable_tray_notification'])
from calibre.translations.compiled import translations from calibre.translations.compiled import translations
@ -426,17 +429,17 @@ class ConfigDialog(QDialog, Ui_Dialog):
self.pdf_metadata.setChecked(prefs['read_file_metadata']) self.pdf_metadata.setChecked(prefs['read_file_metadata'])
added_html = False exts = set([])
for ext in self.book_exts: for ext in BOOK_EXTENSIONS:
ext = ext.lower() ext = ext.lower()
ext = re.sub(r'(x{0,1})htm(l{0,1})', 'html', ext) ext = re.sub(r'(x{0,1})htm(l{0,1})', 'html', ext)
if ext == 'lrf' or is_supported('book.'+ext): if ext == 'lrf' or is_supported('book.'+ext):
if ext == 'html' and added_html: exts.add(ext)
continue
self.viewer.addItem(ext.upper()) for ext in sorted(exts):
self.viewer.item(self.viewer.count()-1).setFlags(Qt.ItemIsEnabled|Qt.ItemIsUserCheckable) self.viewer.addItem(ext.upper())
self.viewer.item(self.viewer.count()-1).setCheckState(Qt.Checked if ext.upper() in config['internally_viewed_formats'] else Qt.Unchecked) self.viewer.item(self.viewer.count()-1).setFlags(Qt.ItemIsEnabled|Qt.ItemIsUserCheckable)
added_html = ext == 'html' 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.viewer.sortItems()
self.start.setEnabled(not getattr(self.server, 'is_running', False)) self.start.setEnabled(not getattr(self.server, 'is_running', False))
self.test.setEnabled(not self.start.isEnabled()) 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()] p = {0:'normal', 1:'high', 2:'low'}[self.priority.currentIndex()]
prefs['worker_process_priority'] = p prefs['worker_process_priority'] = p
prefs['read_file_metadata'] = bool(self.pdf_metadata.isChecked()) prefs['read_file_metadata'] = bool(self.pdf_metadata.isChecked())
prefs['output_format'] = self.output_format.currentText() prefs['output_format'] = unicode(self.output_format.currentText()).upper()
config['save_to_disk_single_format'] = self.book_exts[self.single_format.currentIndex()]
config['cover_flow_queue_length'] = self.cover_browse.value() config['cover_flow_queue_length'] = self.cover_browse.value()
prefs['language'] = str(self.language.itemData(self.language.currentIndex()).toString()) prefs['language'] = str(self.language.itemData(self.language.currentIndex()).toString())
config['systray_icon'] = self.systray_icon.checkState() == Qt.Checked config['systray_icon'] = self.systray_icon.checkState() == Qt.Checked

View File

@ -147,19 +147,6 @@
<item> <item>
<layout class="QGridLayout" name="gridLayout_2"> <layout class="QGridLayout" name="gridLayout_2">
<item row="1" column="0"> <item row="1" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Format for &amp;single file save:</string>
</property>
<property name="buddy">
<cstring>single_format</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="single_format"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_2"> <widget class="QLabel" name="label_2">
<property name="text"> <property name="text">
<string>Default network &amp;timeout:</string> <string>Default network &amp;timeout:</string>
@ -169,7 +156,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="1"> <item row="1" column="1">
<widget class="QSpinBox" name="timeout"> <widget class="QSpinBox" name="timeout">
<property name="toolTip"> <property name="toolTip">
<string>Set the default timeout for network fetches (i.e. anytime we go out to the internet to get information)</string> <string>Set the default timeout for network fetches (i.e. anytime we go out to the internet to get information)</string>
@ -188,10 +175,10 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="3" column="1"> <item row="2" column="1">
<widget class="QComboBox" name="language"/> <widget class="QComboBox" name="language"/>
</item> </item>
<item row="3" column="0"> <item row="2" column="0">
<widget class="QLabel" name="label_7"> <widget class="QLabel" name="label_7">
<property name="text"> <property name="text">
<string>Choose &amp;language (requires restart):</string> <string>Choose &amp;language (requires restart):</string>
@ -201,7 +188,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="1"> <item row="3" column="1">
<widget class="QComboBox" name="priority"> <widget class="QComboBox" name="priority">
<item> <item>
<property name="text"> <property name="text">
@ -220,7 +207,7 @@
</item> </item>
</widget> </widget>
</item> </item>
<item row="4" column="0"> <item row="3" column="0">
<widget class="QLabel" name="priority_label"> <widget class="QLabel" name="priority_label">
<property name="text"> <property name="text">
<string>Job &amp;priority:</string> <string>Job &amp;priority:</string>

View File

@ -20,6 +20,7 @@ from calibre.utils.search_query_parser import SearchQueryParser
from calibre.utils.pyparsing import ParseException from calibre.utils.pyparsing import ParseException
from calibre.gui2 import NONE, error_dialog, config as gconf from calibre.gui2 import NONE, error_dialog, config as gconf
from calibre.utils.config import DynamicConfig from calibre.utils.config import DynamicConfig
from calibre.ptempfile import PersistentTemporaryFile
from calibre.gui2.dialogs.user_profiles import UserProfiles from calibre.gui2.dialogs.user_profiles import UserProfiles
config = DynamicConfig('scheduler') config = DynamicConfig('scheduler')
@ -522,8 +523,12 @@ class Scheduler(QObject):
self.recipes.remove(recipe) self.recipes.remove(recipe)
save_recipes(self.recipes) save_recipes(self.recipes)
return return
pt = PersistentTemporaryFile('_builtin.recipe')
pt.write(script)
pt.close()
script = pt.name
except ValueError: except ValueError:
script = recipe.title script = recipe.title + '.recipe'
self.debug('\tQueueing:', recipe) self.debug('\tQueueing:', recipe)
self.main.download_scheduled_recipe(recipe, script, self.recipe_downloaded) self.main.download_scheduled_recipe(recipe, script, self.recipe_downloaded)
self.queue.add(recipe) self.queue.add(recipe)

View File

@ -47,6 +47,19 @@ from calibre.library.database2 import LibraryDatabase2, CoverCache
from calibre.parallel import JobKilled from calibre.parallel import JobKilled
from calibre.gui2.dialogs.confirm_delete import confirm 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): class Main(MainWindow, Ui_MainWindow, DeviceGUI):
'The main GUI' 'The main GUI'
@ -201,8 +214,12 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.save_menu = QMenu() self.save_menu = QMenu()
self.save_menu.addAction(_('Save to disk')) self.save_menu.addAction(_('Save to disk'))
self.save_menu.addAction(_('Save to disk in a single directory')) self.save_menu.addAction(_('Save to disk in a single directory'))
self.save_menu.addAction(_('Save only %s format to disk')%\ self.save_menu.addAction(_('Save only %s format to disk')%
config.get('save_to_disk_single_format').upper()) 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 = QMenu()
self.view_menu.addAction(_('View')) self.view_menu.addAction(_('View'))
@ -856,10 +873,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
_('Failed to download metadata for the following:'), _('Failed to download metadata for the following:'),
details, self).exec_() details, self).exec_()
else: else:
err = _('<b>Failed to download metadata:')+\ err = _('Failed to download metadata:')
'</b><br><pre>'+x.tb+'</pre>' error_dialog(self, _('Error'), err, det_msg=x.tb).exec_()
error_dialog(self, _('Error'), err,
show=True)
@ -912,7 +927,10 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
############################## Save to disk ################################ ############################## Save to disk ################################
def save_single_format_to_disk(self, checked): 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): def save_to_single_dir(self, checked):
self.save_to_disk(checked, True) self.save_to_disk(checked, True)
@ -921,10 +939,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
rows = self.current_view().selectionModel().selectedRows() rows = self.current_view().selectionModel().selectedRows()
if not rows or len(rows) == 0: if not rows or len(rows) == 0:
d = error_dialog(self, _('Cannot save to disk'), return error_dialog(self, _('Cannot save to disk'),
_('No books selected')) _('No books selected'), show=True)
d.exec_()
return
progress = ProgressDialog(_('Saving to disk...'), min=0, max=len(rows), progress = ProgressDialog(_('Saving to disk...'), min=0, max=len(rows),
parent=self) parent=self)
@ -1266,8 +1282,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
config['show_text_in_toolbar'] else \ config['show_text_in_toolbar'] else \
Qt.ToolButtonIconOnly) Qt.ToolButtonIconOnly)
self.save_menu.actions()[2].setText( self.save_menu.actions()[2].setText(
_('Save only %s format to disk')%config.get( _('Save only %s format to disk')%
'save_to_disk_single_format').upper()) prefs['output_format'].upper())
if self.library_path != d.database_location: if self.library_path != d.database_location:
try: try:
newloc = d.database_location newloc = d.database_location
@ -1414,8 +1430,9 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
return return
if isinstance(job.exception, JobKilled): if isinstance(job.exception, JobKilled):
return return
error_dialog(self, _('Conversion Error'), job.gui_text(), error_dialog(self, _('Conversion Error'),
show=True) _('Failed to process')+': '+unicode(job.description),
det_msg=job.console_text()).exec_()
def initialize_database(self): def initialize_database(self):

View File

@ -42,7 +42,7 @@
<property name="maximumSize"> <property name="maximumSize">
<size> <size>
<width>16777215</width> <width>16777215</width>
<height>100</height> <height>75</height>
</size> </size>
</property> </property>
<property name="verticalScrollBarPolicy"> <property name="verticalScrollBarPolicy">

View File

@ -7,6 +7,7 @@ from PyQt4.Qt import QMainWindow, QString, Qt, QFont, QCoreApplication, SIGNAL,\
QAction, QMenu, QMenuBar, QIcon QAction, QMenu, QMenuBar, QIcon
from calibre.gui2.dialogs.conversion_error import ConversionErrorDialog from calibre.gui2.dialogs.conversion_error import ConversionErrorDialog
from calibre.utils.config import OptionParser from calibre.utils.config import OptionParser
from calibre.gui2 import error_dialog
def option_parser(usage='''\ def option_parser(usage='''\
Usage: %prog [options] Usage: %prog [options]
@ -19,26 +20,26 @@ Launch the Graphical User Interface
return parser return parser
class DebugWindow(ConversionErrorDialog): class DebugWindow(ConversionErrorDialog):
def __init__(self, parent): def __init__(self, parent):
ConversionErrorDialog.__init__(self, parent, 'Console output', '') ConversionErrorDialog.__init__(self, parent, 'Console output', '')
self.setModal(Qt.NonModal) self.setModal(Qt.NonModal)
font = QFont() font = QFont()
font.setStyleHint(QFont.TypeWriter) font.setStyleHint(QFont.TypeWriter)
self.text.setFont(font) self.text.setFont(font)
def write(self, msg): def write(self, msg):
self.text.setPlainText(self.text.toPlainText()+QString(msg)) self.text.setPlainText(self.text.toPlainText()+QString(msg))
def flush(self): def flush(self):
pass pass
class MainWindow(QMainWindow): class MainWindow(QMainWindow):
___menu_bar = None ___menu_bar = None
___menu = None ___menu = None
__actions = [] __actions = []
@classmethod @classmethod
def create_application_menubar(cls): def create_application_menubar(cls):
mb = QMenuBar(None) mb = QMenuBar(None)
@ -50,8 +51,8 @@ class MainWindow(QMainWindow):
mb.addMenu(menu) mb.addMenu(menu)
cls.___menu_bar = mb cls.___menu_bar = mb
cls.___menu = menu cls.___menu = menu
@classmethod @classmethod
def get_menubar_actions(cls): def get_menubar_actions(cls):
preferences_action = QAction(QIcon(':/images/config.svg'), _('&Preferences'), None) preferences_action = QAction(QIcon(':/images/config.svg'), _('&Preferences'), None)
@ -59,7 +60,7 @@ class MainWindow(QMainWindow):
preferences_action.setMenuRole(QAction.PreferencesRole) preferences_action.setMenuRole(QAction.PreferencesRole)
quit_action.setMenuRole(QAction.QuitRole) quit_action.setMenuRole(QAction.QuitRole)
return preferences_action, quit_action return preferences_action, quit_action
def __init__(self, opts, parent=None): def __init__(self, opts, parent=None):
QMainWindow.__init__(self, parent) QMainWindow.__init__(self, parent)
app = QCoreApplication.instance() app = QCoreApplication.instance()
@ -69,19 +70,18 @@ class MainWindow(QMainWindow):
self.__console_redirect = DebugWindow(self) self.__console_redirect = DebugWindow(self)
sys.stdout = sys.stderr = self.__console_redirect sys.stdout = sys.stderr = self.__console_redirect
self.__console_redirect.show() self.__console_redirect.show()
def unix_signal(self, signal): def unix_signal(self, signal):
print 'Received signal:', repr(signal) print 'Received signal:', repr(signal)
def unhandled_exception(self, type, value, tb): def unhandled_exception(self, type, value, tb):
try: try:
sio = StringIO.StringIO() sio = StringIO.StringIO()
traceback.print_exception(type, value, tb, file=sio) traceback.print_exception(type, value, tb, file=sio)
fe = sio.getvalue() fe = sio.getvalue()
print >>sys.stderr, fe print >>sys.stderr, fe
msg = '<p><b>' + unicode(str(value), 'utf8', 'replace') + '</b></p>' msg = unicode(str(value), 'utf8', 'replace')
msg += '<p>Detailed <b>traceback</b>:<pre>'+fe+'</pre>' error_dialog(self, _('ERROR: Unhandled exception'), msg, det_msg=fe,
d = ConversionErrorDialog(self, _('ERROR: Unhandled exception'), msg) show=True)
d.exec_()
except: except:
pass pass

View File

@ -7,22 +7,22 @@ __docformat__ = 'restructuredtext en'
Logic for setting up conversion jobs Logic for setting up conversion jobs
''' '''
import cPickle, os import cPickle
from PyQt4.Qt import QDialog from PyQt4.Qt import QDialog
from calibre.ptempfile import PersistentTemporaryFile from calibre.ptempfile import PersistentTemporaryFile
from calibre.gui2 import warning_dialog 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 NoSupportedInputFormats
from calibre.gui2.convert.single import Config as SingleConfig from calibre.gui2.convert.single import Config as SingleConfig
from calibre.gui2.convert.bulk import BulkConfig 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): def convert_single_ebook(parent, db, book_ids, auto_conversion=False, out_format=None):
changed = False changed = False
jobs = [] jobs = []
bad = [] bad = []
total = len(book_ids) total = len(book_ids)
if total == 0: if total == 0:
return None, None, None return None, None, None
@ -33,23 +33,23 @@ def convert_single_ebook(parent, db, book_ids, auto_conversion=False, out_format
try: try:
d = SingleConfig(parent, db, book_id, None, out_format) d = SingleConfig(parent, db, book_id, None, out_format)
if auto_conversion: if auto_conversion:
d.accept() d.accept()
result = QDialog.Accepted result = QDialog.Accepted
else: else:
result = d.exec_() result = d.exec_()
if result == QDialog.Accepted: if result == QDialog.Accepted:
mi = db.get_metadata(book_id, True) mi = db.get_metadata(book_id, True)
in_file = db.format_abspath(book_id, d.input_format, True) in_file = db.format_abspath(book_id, d.input_format, True)
out_file = PersistentTemporaryFile('.' + d.output_format) out_file = PersistentTemporaryFile('.' + d.output_format)
out_file.write(d.output_format) out_file.write(d.output_format)
out_file.close() out_file.close()
desc = _('Convert book %d of %d (%s)') % (i + 1, total, repr(mi.title)) desc = _('Convert book %d of %d (%s)') % (i + 1, total, repr(mi.title))
recs = cPickle.loads(d.recommendations) recs = cPickle.loads(d.recommendations)
args = [in_file, out_file.name, recs] args = [in_file, out_file.name, recs]
temp_files = [out_file] temp_files = [out_file]
@ -76,7 +76,7 @@ def convert_bulk_ebook(parent, db, book_ids, out_format=None):
changed = False changed = False
jobs = [] jobs = []
bad = [] bad = []
total = len(book_ids) total = len(book_ids)
if total == 0: if total == 0:
return None, None, None return None, None, None
@ -95,16 +95,16 @@ def convert_bulk_ebook(parent, db, book_ids, out_format=None):
try: try:
d = SingleConfig(parent, db, book_id, None, output_format) d = SingleConfig(parent, db, book_id, None, output_format)
d.accept() d.accept()
mi = db.get_metadata(book_id, True) mi = db.get_metadata(book_id, True)
in_file = db.format_abspath(book_id, d.input_format, True) in_file = db.format_abspath(book_id, d.input_format, True)
out_file = PersistentTemporaryFile('.' + output_format) out_file = PersistentTemporaryFile('.' + output_format)
out_file.write(output_format) out_file.write(output_format)
out_file.close() out_file.close()
desc = _('Convert book %d of %d (%s)') % (i + 1, total, repr(mi.title)) desc = _('Convert book %d of %d (%s)') % (i + 1, total, repr(mi.title))
args = [in_file, out_file.name, recs] args = [in_file, out_file.name, recs]
temp_files = [out_file] temp_files = [out_file]
jobs.append(('gui_convert', args, desc, d.output_format.upper(), book_id, temp_files)) 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 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): def fetch_scheduled_recipe(recipe, script):
from calibre.gui2.dialogs.scheduler import config from calibre.gui2.dialogs.scheduler import config
fmt = prefs['output_format'].lower() 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() pt.close()
args = ['feeds2%s'%fmt.lower(), '--output', pt.name, '--debug'] args = ['ebook-convert', script, pt.name, '-vv']
if recipe.needs_subscription: if recipe.needs_subscription:
x = config.get('recipe_account_info_%s'%recipe.id, False) x = config.get('recipe_account_info_%s'%recipe.id, False)
if not x: if not x:
raise ValueError(_('You must set a username and password for %s')%recipe.title) raise ValueError(_('You must set a username and password for %s')%recipe.title)
args.extend(['--username', x[0], '--password', x[1]]) 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): return 'ebook-convert', [args], _('Fetch news from ')+recipe.title, fmt.upper(), [pt]
fmt = prefs['output_format'].lower()
return _fetch_news(data, fmt)

View File

@ -19,7 +19,6 @@ from calibre.gui2 import Application, ORG_NAME, APP_UID, choose_files, \
info_dialog, error_dialog info_dialog, error_dialog
from calibre.ebooks.oeb.iterator import EbookIterator from calibre.ebooks.oeb.iterator import EbookIterator
from calibre.ebooks import DRMError from calibre.ebooks import DRMError
from calibre.gui2.dialogs.conversion_error import ConversionErrorDialog
from calibre.constants import islinux from calibre.constants import islinux
from calibre.utils.config import Config, StringConfig from calibre.utils.config import Config, StringConfig
from calibre.gui2.library import SearchBox from calibre.gui2.library import SearchBox
@ -543,8 +542,8 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
if isinstance(worker.exception, DRMError): if isinstance(worker.exception, DRMError):
error_dialog(self, _('DRM Error'), _('<p>This book is protected by <a href="%s">DRM</a>')%'http://wiki.mobileread.com/wiki/DRM').exec_() error_dialog(self, _('DRM Error'), _('<p>This book is protected by <a href="%s">DRM</a>')%'http://wiki.mobileread.com/wiki/DRM').exec_()
else: else:
ConversionErrorDialog(self, _('Could not open ebook'), error_dialog(self, _('Could not open ebook'),
_('<b>%s</b><br/><p>%s</p>')%(worker.exception, worker.traceback.replace('\n', '<br>')), show=True) unicode(worker.exception), det_msg=worker.traceback, show=True)
self.close_progress_indicator() self.close_progress_indicator()
else: else:
self.metadata.show_opf(self.iterator.opf) self.metadata.show_opf(self.iterator.opf)

View File

@ -14,7 +14,7 @@ from optparse import IndentedHelpFormatter
from PyQt4.QtCore import QString from PyQt4.QtCore import QString
from calibre.constants import terminal_controller, iswindows, isosx, \ from calibre.constants import terminal_controller, iswindows, isosx, \
__appname__, __version__, __author__, plugins __appname__, __version__, __author__, plugins
from calibre.utils.lock import LockError, ExclusiveFile from calibre.utils.lock import LockError, ExclusiveFile
from collections import defaultdict from collections import defaultdict
if os.environ.has_key('CALIBRE_CONFIG_DIRECTORY'): if os.environ.has_key('CALIBRE_CONFIG_DIRECTORY'):
@ -38,14 +38,14 @@ def make_config_dir():
class CustomHelpFormatter(IndentedHelpFormatter): class CustomHelpFormatter(IndentedHelpFormatter):
def format_usage(self, usage): def format_usage(self, usage):
return _("%sUsage%s: %s\n") % (terminal_controller.BLUE, terminal_controller.NORMAL, usage) return _("%sUsage%s: %s\n") % (terminal_controller.BLUE, terminal_controller.NORMAL, usage)
def format_heading(self, heading): 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) "", heading, terminal_controller.NORMAL)
def format_option(self, option): def format_option(self, option):
result = [] result = []
opts = self.option_strings[option] opts = self.option_strings[option]
@ -55,14 +55,14 @@ class CustomHelpFormatter(IndentedHelpFormatter):
terminal_controller.GREEN+opts+terminal_controller.NORMAL) terminal_controller.GREEN+opts+terminal_controller.NORMAL)
indent_first = self.help_position indent_first = self.help_position
else: # start help on same line as opts 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) terminal_controller.GREEN + opts + terminal_controller.NORMAL)
indent_first = 0 indent_first = 0
result.append(opts) result.append(opts)
if option.help: if option.help:
help_text = self.expand_default(option).split('\n') help_text = self.expand_default(option).split('\n')
help_lines = [] help_lines = []
for line in help_text: for line in help_text:
help_lines.extend(textwrap.wrap(line, self.help_width)) help_lines.extend(textwrap.wrap(line, self.help_width))
result.append("%*s%s\n" % (indent_first, "", help_lines[0])) result.append("%*s%s\n" % (indent_first, "", help_lines[0]))
@ -74,7 +74,7 @@ class CustomHelpFormatter(IndentedHelpFormatter):
class OptionParser(_OptionParser): class OptionParser(_OptionParser):
def __init__(self, def __init__(self,
usage='%prog [options] filename', usage='%prog [options] filename',
version='%%prog (%s %s)'%(__appname__, __version__), version='%%prog (%s %s)'%(__appname__, __version__),
@ -85,16 +85,16 @@ class OptionParser(_OptionParser):
usage = textwrap.dedent(usage) usage = textwrap.dedent(usage)
usage += '''\n\nWhenever you pass arguments to %prog that have spaces in them, '''\ usage += '''\n\nWhenever you pass arguments to %prog that have spaces in them, '''\
'''enclose the arguments in quotation marks.''' '''enclose the arguments in quotation marks.'''
_OptionParser.__init__(self, usage=usage, version=version, epilog=epilog, _OptionParser.__init__(self, usage=usage, version=version, epilog=epilog,
formatter=CustomHelpFormatter(), formatter=CustomHelpFormatter(),
conflict_handler=conflict_handler, **kwds) conflict_handler=conflict_handler, **kwds)
self.gui_mode = gui_mode self.gui_mode = gui_mode
def error(self, msg): def error(self, msg):
if self.gui_mode: if self.gui_mode:
raise Exception(msg) raise Exception(msg)
_OptionParser.error(self, msg) _OptionParser.error(self, msg)
def merge(self, parser): def merge(self, parser):
''' '''
Add options from parser to self. In case of conflicts, conflicting options from 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) opts = list(parser.option_list)
groups = list(parser.option_groups) groups = list(parser.option_groups)
def merge_options(options, container): def merge_options(options, container):
for opt in deepcopy(options): for opt in deepcopy(options):
if not self.has_option(opt.get_opt_string()): if not self.has_option(opt.get_opt_string()):
container.add_option(opt) container.add_option(opt)
merge_options(opts, self) merge_options(opts, self)
for group in groups: for group in groups:
g = self.add_option_group(group.title) g = self.add_option_group(group.title)
merge_options(group.option_list, g) merge_options(group.option_list, g)
def subsume(self, group_name, msg=''): def subsume(self, group_name, msg=''):
''' '''
Move all existing options into a subgroup named Move all existing options into a subgroup named
@ -125,7 +125,7 @@ class OptionParser(_OptionParser):
for opt in opts: for opt in opts:
self.remove_option(opt.get_opt_string()) self.remove_option(opt.get_opt_string())
subgroup.add_option(opt) subgroup.add_option(opt)
def options_iter(self): def options_iter(self):
for opt in self.option_list: for opt in self.option_list:
if str(opt).strip(): if str(opt).strip():
@ -134,12 +134,12 @@ class OptionParser(_OptionParser):
for opt in gr.option_list: for opt in gr.option_list:
if str(opt).strip(): if str(opt).strip():
yield opt yield opt
def option_by_dest(self, dest): def option_by_dest(self, dest):
for opt in self.options_iter(): for opt in self.options_iter():
if opt.dest == dest: if opt.dest == dest:
return opt return opt
def merge_options(self, lower, upper): def merge_options(self, lower, upper):
''' '''
Merge options in lower and upper option lists into 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 \ if lower.__dict__[dest] != opt.default and \
upper.__dict__[dest] == opt.default: upper.__dict__[dest] == opt.default:
upper.__dict__[dest] = lower.__dict__[dest] upper.__dict__[dest] = lower.__dict__[dest]
class Option(object): 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): check=None, group=None, default=None, action=None, metavar=None):
if choices: if choices:
type = 'choice' type = 'choice'
self.name = name self.name = name
self.switches = switches self.switches = switches
self.help = help.replace('%default', repr(default)) if help else None self.help = help.replace('%default', repr(default)) if help else None
@ -172,40 +172,40 @@ class Option(object):
self.type = 'float' self.type = 'float'
elif isinstance(default, int) and not isinstance(default, bool): elif isinstance(default, int) and not isinstance(default, bool):
self.type = 'int' self.type = 'int'
self.choices = choices self.choices = choices
self.check = check self.check = check
self.group = group self.group = group
self.default = default self.default = default
self.action = action self.action = action
self.metavar = metavar self.metavar = metavar
def __eq__(self, other): def __eq__(self, other):
return self.name == getattr(other, 'name', other) return self.name == getattr(other, 'name', other)
def __repr__(self): def __repr__(self):
return 'Option: '+self.name return 'Option: '+self.name
def __str__(self): def __str__(self):
return repr(self) return repr(self)
class OptionValues(object): class OptionValues(object):
def copy(self): def copy(self):
return deepcopy(self) return deepcopy(self)
class OptionSet(object): 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) re.DOTALL|re.IGNORECASE)
def __init__(self, description=''): def __init__(self, description=''):
self.description = description self.description = description
self.preferences = [] self.preferences = []
self.group_list = [] self.group_list = []
self.groups = {} self.groups = {}
self.set_buffer = {} self.set_buffer = {}
def has_option(self, name_or_option_object): def has_option(self, name_or_option_object):
if name_or_option_object in self.preferences: if name_or_option_object in self.preferences:
return True return True
@ -213,14 +213,14 @@ class OptionSet(object):
if p.name == name_or_option_object: if p.name == name_or_option_object:
return True return True
return False return False
def add_group(self, name, description=''): def add_group(self, name, description=''):
if name in self.group_list: if name in self.group_list:
raise ValueError('A group by the name %s already exists in this set'%name) raise ValueError('A group by the name %s already exists in this set'%name)
self.groups[name] = description self.groups[name] = description
self.group_list.append(name) self.group_list.append(name)
return partial(self.add_opt, group=name) return partial(self.add_opt, group=name)
def update(self, other): def update(self, other):
for name in other.groups.keys(): for name in other.groups.keys():
self.groups[name] = other.groups[name] self.groups[name] = other.groups[name]
@ -230,7 +230,7 @@ class OptionSet(object):
if pref in self.preferences: if pref in self.preferences:
self.preferences.remove(pref) self.preferences.remove(pref)
self.preferences.append(pref) self.preferences.append(pref)
def smart_update(self, opts1, opts2): def smart_update(self, opts1, opts2):
''' '''
Updates the preference values in opts1 using only the non-default preference values in 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) new = getattr(opts2, pref.name, pref.default)
if new != pref.default: if new != pref.default:
setattr(opts1, pref.name, new) setattr(opts1, pref.name, new)
def remove_opt(self, name): def remove_opt(self, name):
if name in self.preferences: if name in self.preferences:
self.preferences.remove(name) 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): group=None, default=None, action=None, metavar=None):
''' '''
Add an option to this section. Add an option to this section.
:param name: The name of this option. Must be a valid Python identifier. :param name: The name of this option. Must be a valid Python identifier.
Must also be unique in this OptionSet and all its subsets. Must also be unique in this OptionSet and all its subsets.
:param switches: List of command line switches for this option :param switches: List of command line switches for this option
(as supplied to :module:`optparse`). If empty, this (as supplied to :module:`optparse`). If empty, this
option will not be added to the command line parser. option will not be added to the command line parser.
:param help: Help text. :param help: Help text.
:param type: Type checking of option values. Supported types are: :param type: Type checking of option values. Supported types are:
`None, 'choice', 'complex', 'float', 'int', 'string'`. `None, 'choice', 'complex', 'float', 'int', 'string'`.
:param choices: List of strings or `None`. :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`. have created this group with a call to :method:`add_group`.
:param default: The default value for this option. :param default: The default value for this option.
:param action: The action to pass to optparse. Supported values are: :param action: The action to pass to optparse. Supported values are:
`None, 'count'`. For choices and boolean options, `None, 'count'`. For choices and boolean options,
action is automatically set correctly. 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) group=group, default=default, action=action, metavar=None)
if group is not None and group not in self.groups.keys(): if group is not None and group not in self.groups.keys():
raise ValueError('Group %s has not been added to this section'%group) raise ValueError('Group %s has not been added to this section'%group)
if pref in self.preferences: if pref in self.preferences:
raise ValueError('An option with the name %s already exists in this set.'%name) raise ValueError('An option with the name %s already exists in this set.'%name)
self.preferences.append(pref) self.preferences.append(pref)
def option_parser(self, user_defaults=None, usage='', gui_mode=False): def option_parser(self, user_defaults=None, usage='', gui_mode=False):
parser = OptionParser(usage, gui_mode=gui_mode) parser = OptionParser(usage, gui_mode=gui_mode)
groups = defaultdict(lambda : parser) groups = defaultdict(lambda : parser)
for group, desc in self.groups.items(): for group, desc in self.groups.items():
groups[group] = parser.add_option_group(group.upper(), desc) groups[group] = parser.add_option_group(group.upper(), desc)
for pref in self.preferences: for pref in self.preferences:
if not pref.switches: if not pref.switches:
continue continue
@ -299,16 +299,16 @@ class OptionSet(object):
action=action, action=action,
) )
g.add_option(*pref.switches, **args) g.add_option(*pref.switches, **args)
return parser return parser
def get_override_section(self, src): def get_override_section(self, src):
match = self.OVERRIDE_PAT.search(src) match = self.OVERRIDE_PAT.search(src)
if match: if match:
return match.group() return match.group()
return '' return ''
def parse_string(self, src): def parse_string(self, src):
options = {'cPickle':cPickle} options = {'cPickle':cPickle}
if not isinstance(src, unicode): if not isinstance(src, unicode):
@ -327,9 +327,9 @@ class OptionSet(object):
if callable(formatter): if callable(formatter):
val = formatter(val) val = formatter(val)
setattr(opts, pref.name, val) setattr(opts, pref.name, val)
return opts return opts
def render_group(self, name, desc, opts): def render_group(self, name, desc, opts):
prefs = [pref for pref in self.preferences if pref.group == name] prefs = [pref for pref in self.preferences if pref.group == name]
lines = ['### Begin group: %s'%(name if name else 'DEFAULT')] lines = ['### Begin group: %s'%(name if name else 'DEFAULT')]
@ -340,11 +340,11 @@ class OptionSet(object):
lines.append('# '+pref.name.replace('_', ' ')) lines.append('# '+pref.name.replace('_', ' '))
if pref.help: if pref.help:
lines += map(lambda x: '# ' + x, pref.help.split('\n')) 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)))) self.serialize_opt(getattr(opts, pref.name, pref.default))))
lines.append(' ') lines.append(' ')
return '\n'.join(lines) return '\n'.join(lines)
def serialize_opt(self, val): def serialize_opt(self, val):
if val is val is True or val is False or val is None or \ if val is val is True or val is False or val is None or \
isinstance(val, (int, float, long, basestring)): isinstance(val, (int, float, long, basestring)):
@ -353,7 +353,7 @@ class OptionSet(object):
return repr(unicode(val)) return repr(unicode(val))
pickle = cPickle.dumps(val, -1) pickle = cPickle.dumps(val, -1)
return 'cPickle.loads(%s)'%repr(pickle) return 'cPickle.loads(%s)'%repr(pickle)
def serialize(self, opts): def serialize(self, opts):
src = '# %s\n\n'%(self.description.replace('\n', '\n# ')) src = '# %s\n\n'%(self.description.replace('\n', '\n# '))
groups = [self.render_group(name, self.groups.get(name, ''), opts) \ groups = [self.render_group(name, self.groups.get(name, ''), opts) \
@ -361,34 +361,34 @@ class OptionSet(object):
return src + '\n\n'.join(groups) return src + '\n\n'.join(groups)
class ConfigInterface(object): class ConfigInterface(object):
def __init__(self, description): def __init__(self, description):
self.option_set = OptionSet(description=description) self.option_set = OptionSet(description=description)
self.add_opt = self.option_set.add_opt self.add_opt = self.option_set.add_opt
self.add_group = self.option_set.add_group self.add_group = self.option_set.add_group
self.remove_opt = self.remove = self.option_set.remove_opt self.remove_opt = self.remove = self.option_set.remove_opt
self.parse_string = self.option_set.parse_string self.parse_string = self.option_set.parse_string
def update(self, other): def update(self, other):
self.option_set.update(other.option_set) self.option_set.update(other.option_set)
def option_parser(self, usage='', gui_mode=False): 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) usage=usage, gui_mode=gui_mode)
def smart_update(self, opts1, opts2): def smart_update(self, opts1, opts2):
self.option_set.smart_update(opts1, opts2) self.option_set.smart_update(opts1, opts2)
class Config(ConfigInterface): class Config(ConfigInterface):
''' '''
A file based configuration. A file based configuration.
''' '''
def __init__(self, basename, description=''): def __init__(self, basename, description=''):
ConfigInterface.__init__(self, description) ConfigInterface.__init__(self, description)
self.config_file_path = os.path.join(config_dir, basename+'.py') self.config_file_path = os.path.join(config_dir, basename+'.py')
def parse(self): def parse(self):
src = '' src = ''
if os.path.exists(self.config_file_path): if os.path.exists(self.config_file_path):
@ -398,7 +398,7 @@ class Config(ConfigInterface):
except LockError: except LockError:
raise IOError('Could not lock config file: %s'%self.config_file_path) raise IOError('Could not lock config file: %s'%self.config_file_path)
return self.option_set.parse_string(src) return self.option_set.parse_string(src)
def as_string(self): def as_string(self):
if not os.path.exists(self.config_file_path): if not os.path.exists(self.config_file_path):
return '' return ''
@ -407,7 +407,7 @@ class Config(ConfigInterface):
return f.read().decode('utf-8') return f.read().decode('utf-8')
except LockError: except LockError:
raise IOError('Could not lock config file: %s'%self.config_file_path) raise IOError('Could not lock config file: %s'%self.config_file_path)
def set(self, name, val): def set(self, name, val):
if not self.option_set.has_option(name): if not self.option_set.has_option(name):
raise ValueError('The option %s is not defined.'%name) raise ValueError('The option %s is not defined.'%name)
@ -427,19 +427,19 @@ class Config(ConfigInterface):
f.write(src) f.write(src)
except LockError: except LockError:
raise IOError('Could not lock config file: %s'%self.config_file_path) raise IOError('Could not lock config file: %s'%self.config_file_path)
class StringConfig(ConfigInterface): class StringConfig(ConfigInterface):
''' '''
A string based configuration A string based configuration
''' '''
def __init__(self, src, description=''): def __init__(self, src, description=''):
ConfigInterface.__init__(self, description) ConfigInterface.__init__(self, description)
self.src = src self.src = src
def parse(self): def parse(self):
return self.option_set.parse_string(self.src) return self.option_set.parse_string(self.src)
def set(self, name, val): def set(self, name, val):
if not self.option_set.has_option(name): if not self.option_set.has_option(name):
raise ValueError('The option %s is not defined.'%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 A Proxy to minimize file reads for widely used config settings
''' '''
def __init__(self, config): def __init__(self, config):
self.__config = config self.__config = config
self.__opts = None self.__opts = None
def refresh(self): def refresh(self):
self.__opts = self.__config.parse() self.__opts = self.__config.parse()
def __getitem__(self, key): def __getitem__(self, key):
return self.get(key) return self.get(key)
def __setitem__(self, key, val): def __setitem__(self, key, val):
return self.set(key, val) return self.set(key, val)
def get(self, key): def get(self, key):
if self.__opts is None: if self.__opts is None:
self.refresh() self.refresh()
return getattr(self.__opts, key) return getattr(self.__opts, key)
def set(self, key, val): def set(self, key, val):
if self.__opts is None: if self.__opts is None:
self.refresh() self.refresh()
setattr(self.__opts, key, val) setattr(self.__opts, key, val)
return self.__config.set(key, val) return self.__config.set(key, val)
class DynamicConfig(dict): class DynamicConfig(dict):
''' '''
@ -489,7 +489,7 @@ class DynamicConfig(dict):
self.name = name self.name = name
self.file_path = os.path.join(config_dir, name+'.pickle') self.file_path = os.path.join(config_dir, name+'.pickle')
self.refresh() self.refresh()
def refresh(self): def refresh(self):
d = {} d = {}
if os.path.exists(self.file_path): if os.path.exists(self.file_path):
@ -503,20 +503,20 @@ class DynamicConfig(dict):
d = {} d = {}
self.clear() self.clear()
self.update(d) self.update(d)
def __getitem__(self, key): def __getitem__(self, key):
try: try:
return dict.__getitem__(self, key) return dict.__getitem__(self, key)
except KeyError: except KeyError:
return None return None
def __setitem__(self, key, val): def __setitem__(self, key, val):
dict.__setitem__(self, key, val) dict.__setitem__(self, key, val)
self.commit() self.commit()
def set(self, key, val): def set(self, key, val):
self.__setitem__(key, val) self.__setitem__(key, val)
def commit(self): def commit(self):
if hasattr(self, 'file_path') and self.file_path: if hasattr(self, 'file_path') and self.file_path:
if not os.path.exists(self.file_path): if not os.path.exists(self.file_path):
@ -526,17 +526,17 @@ class DynamicConfig(dict):
f.seek(0) f.seek(0)
f.truncate() f.truncate()
f.write(raw) f.write(raw)
dynamic = DynamicConfig() dynamic = DynamicConfig()
def _prefs(): def _prefs():
c = Config('global', 'calibre wide preferences') c = Config('global', 'calibre wide preferences')
c.add_opt('database_path', c.add_opt('database_path',
default=os.path.expanduser('~/library1.db'), default=os.path.expanduser('~/library1.db'),
help=_('Path to the database in which books are stored')) help=_('Path to the database in which books are stored'))
c.add_opt('filename_pattern', default=ur'(?P<title>.+) - (?P<author>[^_]+)', c.add_opt('filename_pattern', default=ur'(?P<title>.+) - (?P<author>[^_]+)',
help=_('Pattern to guess metadata from filenames')) 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')) help=_('Access key for isbndb.com'))
c.add_opt('network_timeout', default=5, c.add_opt('network_timeout', default=5,
help=_('Default timeout for network operations (seconds)')) 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')) help=_('Path to directory in which your library of books is stored'))
c.add_opt('language', default=None, c.add_opt('language', default=None,
help=_('The language in which to display the user interface')) 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.')) help=_('The default output format for ebook conversions.'))
c.add_opt('read_file_metadata', default=True, c.add_opt('read_file_metadata', default=True,
help=_('Read metadata from files')) 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')) help=_('The priority of worker processes'))
c.add_opt('migrated', default=False, help='For Internal use. Don\'t modify.') c.add_opt('migrated', default=False, help='For Internal use. Don\'t modify.')
return c return c
@ -565,11 +565,11 @@ def migrate():
from PyQt4.QtCore import QSettings, QVariant from PyQt4.QtCore import QSettings, QVariant
class Settings(QSettings): class Settings(QSettings):
def __init__(self, name='calibre2'): def __init__(self, name='calibre2'):
QSettings.__init__(self, QSettings.IniFormat, QSettings.UserScope, QSettings.__init__(self, QSettings.IniFormat, QSettings.UserScope,
'kovidgoyal.net', name) 'kovidgoyal.net', name)
def get(self, key, default=None): def get(self, key, default=None):
try: try:
key = str(key) key = str(key)
@ -581,7 +581,7 @@ def migrate():
return cPickle.loads(val) return cPickle.loads(val)
except: except:
return default return default
s, migrated = Settings(), set([]) s, migrated = Settings(), set([])
all_keys = set(map(unicode, s.allKeys())) all_keys = set(map(unicode, s.allKeys()))
from calibre.gui2 import config, dynamic from calibre.gui2 import config, dynamic
@ -599,13 +599,13 @@ def migrate():
pass pass
finally: finally:
migrated.add(key) migrated.add(key)
_migrate('database path', p=prefs) _migrate('database path', p=prefs)
_migrate('filename pattern', p=prefs) _migrate('filename pattern', p=prefs)
_migrate('network timeout', p=prefs) _migrate('network timeout', p=prefs)
_migrate('isbndb.com key', p=prefs) _migrate('isbndb.com key', p=prefs)
_migrate('frequently used directories') _migrate('frequently used directories')
_migrate('send to device by default') _migrate('send to device by default')
_migrate('save to disk single format') _migrate('save to disk single format')
@ -616,33 +616,33 @@ def migrate():
_migrate('cover flow queue length') _migrate('cover flow queue length')
_migrate('LRF conversion defaults') _migrate('LRF conversion defaults')
_migrate('LRF ebook viewer options') _migrate('LRF ebook viewer options')
for key in all_keys - migrated: for key in all_keys - migrated:
if key.endswith(': un') or key.endswith(': pw'): if key.endswith(': un') or key.endswith(': pw'):
_migrate(key, p=dynamic) _migrate(key, p=dynamic)
p.set('migrated', True) p.set('migrated', True)
if __name__ == '__main__': if __name__ == '__main__':
import subprocess import subprocess
from PyQt4.Qt import QByteArray from PyQt4.Qt import QByteArray
c = Config('test', 'test config') c = Config('test', 'test config')
c.add_opt('one', ['-1', '--one'], help="This is option #1") c.add_opt('one', ['-1', '--one'], help="This is option #1")
c.set('one', u'345') c.set('one', u'345')
c.add_opt('two', help="This is option #2") c.add_opt('two', help="This is option #2")
c.set('two', 345) c.set('two', 345)
c.add_opt('three', help="This is option #3") c.add_opt('three', help="This is option #3")
c.set('three', QString(u'aflatoon')) c.set('three', QString(u'aflatoon'))
c.add_opt('four', help="This is option #4") c.add_opt('four', help="This is option #4")
c.set('four', QByteArray('binary aflatoon')) c.set('four', QByteArray('binary aflatoon'))
subprocess.call(['pygmentize', os.path.expanduser('~/.config/calibre/test.py')]) subprocess.call(['pygmentize', os.path.expanduser('~/.config/calibre/test.py')])
opts = c.parse() opts = c.parse()
for i in ('one', 'two', 'three', 'four'): for i in ('one', 'two', 'three', 'four'):
print i, repr(getattr(opts, i)) print i, repr(getattr(opts, i))

View File

@ -18,9 +18,13 @@ class RecipeInput(InputFormatPlugin):
file_types = set(['recipe']) file_types = set(['recipe'])
recommendations = set([ recommendations = set([
('chapter_mark', 'none', OptionRecommendation.HIGH), ('chapter', None, OptionRecommendation.HIGH),
('dont_split_on_page_breaks', True, OptionRecommendation.HIGH), ('dont_split_on_page_breaks', True, OptionRecommendation.HIGH),
('use_auto_toc', False, 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([ options = set([

View File

@ -231,23 +231,23 @@ class BasicNewsRecipe(Recipe):
#: use :member:`extra_css` in your recipe to customize look and feel. #: use :member:`extra_css` in your recipe to customize look and feel.
template_css = u''' template_css = u'''
.article_date { .article_date {
font-size: x-small; color: gray; font-family: monospace; color: gray; font-family: monospace;
} }
.article_description { .article_description {
font-size: small; font-family: sans; text-indent: 0pt; font-family: sans; text-indent: 0pt;
} }
a.article { a.article {
font-weight: bold; font-size: large; font-weight: bold;
} }
a.feed { a.feed {
font-weight: bold; font-size: large; font-weight: bold;
} }
.navbar { .navbar {
font-family:monospace; font-size:8pt font-family:monospace;
} }
''' '''
@ -579,8 +579,9 @@ class BasicNewsRecipe(Recipe):
def feeds2index(self, feeds): def feeds2index(self, feeds):
templ = templates.IndexTemplate() 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, return templ.generate(self.title, self.timefmt, feeds,
extra_css=self.extra_css).render(doctype='xhtml') extra_css=css).render(doctype='xhtml')
@classmethod @classmethod
def description_limiter(cls, src): def description_limiter(cls, src):
@ -631,8 +632,9 @@ class BasicNewsRecipe(Recipe):
templ = templates.FeedTemplate() templ = templates.FeedTemplate()
css = self.template_css + '\n\n' +(self.extra_css if self.extra_css else '')
return templ.generate(feed, self.description_limiter, 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): def _fetch_article(self, url, dir, f, a, num_of_feeds):

View File

@ -5,12 +5,13 @@ import re
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
class DNAIndia(BasicNewsRecipe): class DNAIndia(BasicNewsRecipe):
title = 'DNA India' title = 'DNA India'
description = 'Mumbai news, India news, World news, breaking news' description = 'Mumbai news, India news, World news, breaking news'
__author__ = 'Kovid Goyal' __author__ = 'Kovid Goyal'
language = _('English') language = _('English')
encoding = 'cp1252'
feeds = [ feeds = [
('Top News', 'http://www.dnaindia.com/syndication/rss_topnews.xml'), ('Top News', 'http://www.dnaindia.com/syndication/rss_topnews.xml'),
('Popular News', 'http://www.dnaindia.com/syndication/rss_popular.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'), ('Money', 'http://www.dnaindia.com/syndication/rss,catid-4.xml'),
('Sports', 'http://www.dnaindia.com/syndication/rss,catid-6.xml'), ('Sports', 'http://www.dnaindia.com/syndication/rss,catid-6.xml'),
('After Hours', 'http://www.dnaindia.com/syndication/rss,catid-7.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']}] remove_tags = [{'id':'footer'}, {'class':['bottom', 'categoryHead']}]
def print_version(self, url): def print_version(self, url):
match = re.search(r'newsid=(\d+)', url) match = re.search(r'newsid=(\d+)', url)
if not match: if not match:
return url return url
return 'http://www.dnaindia.com/dnaprint.asp?newsid='+match.group(1) return 'http://www.dnaindia.com/dnaprint.asp?newsid='+match.group(1)
def postprocess_html(self, soup, first_fetch): def postprocess_html(self, soup, first_fetch):
for t in soup.findAll(['table', 'tr', 'td']): for t in soup.findAll(['table', 'tr', 'td']):
t.name = 'div' t.name = 'div'
a = soup.find(href='http://www.3dsyndication.com/') a = soup.find(href='http://www.3dsyndication.com/')
if a is not None: if a is not None:
a.parent.extract() a.parent.extract()
return soup return soup

View File

@ -5,7 +5,6 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import re, string, time import re, string, time
from calibre import strftime from calibre import strftime
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
from calibre.ebooks.BeautifulSoup import BeautifulSoup
class Newsweek(BasicNewsRecipe): class Newsweek(BasicNewsRecipe):
@ -14,15 +13,20 @@ class Newsweek(BasicNewsRecipe):
description = 'Weekly news and current affairs in the US' description = 'Weekly news and current affairs in the US'
no_stylesheets = True no_stylesheets = True
language = _('English') 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')] keep_only_tags = [dict(name='div', id='content')]
remove_tags = [ remove_tags = [
dict(name=['script', 'noscript']), dict(name=['script', 'noscript']),
dict(name='div', attrs={'class':['ad', 'SocialLinks', 'SocialLinksDiv', dict(name='div', attrs={'class':['ad', 'SocialLinks', 'SocialLinksDiv',
'channel', 'bot', 'nav', 'top', 'channel', 'bot', 'nav', 'top',
'EmailArticleBlock', 'EmailArticleBlock',
'comments-and-social-links-wrapper', 'comments-and-social-links-wrapper',
'inline-social-links-wrapper', 'inline-social-links-wrapper',
'inline-social-links', 'inline-social-links',
@ -31,14 +35,14 @@ class Newsweek(BasicNewsRecipe):
dict(id=['ToolBox', 'EmailMain', 'EmailArticle', 'comment-box', dict(id=['ToolBox', 'EmailMain', 'EmailArticle', 'comment-box',
'nw-comments']) 'nw-comments'])
] ]
recursions = 1 recursions = 1
match_regexps = [r'http://www.newsweek.com/id/\S+/page/\d+'] match_regexps = [r'http://www.newsweek.com/id/\S+/page/\d+']
def get_sections(self, soup): def get_sections(self, soup):
sections = [] sections = []
def process_section(img): def process_section(img):
articles = [] articles = []
match = re.search(r'label_([^_.]+)', img['src']) match = re.search(r'label_([^_.]+)', img['src'])
@ -48,25 +52,25 @@ class Newsweek(BasicNewsRecipe):
if title in ['coverstory', 'more', 'tipsheet']: if title in ['coverstory', 'more', 'tipsheet']:
return return
title = string.capwords(title) title = string.capwords(title)
for a in img.parent.findAll('a', href=True): for a in img.parent.findAll('a', href=True):
art, href = a.string, a['href'] art, href = a.string, a['href']
if not re.search('\d+$', href) or not art or 'Preview Article' in art: if not re.search('\d+$', href) or not art or 'Preview Article' in art:
continue continue
articles.append({ articles.append({
'title':art, 'url':href, 'description':'', 'title':art, 'url':href, 'description':'',
'content':'', 'date':'' 'content':'', 'date':''
}) })
sections.append((title, articles)) sections.append((title, articles))
img.parent.extract() img.parent.extract()
for img in soup.findAll(src=re.compile('/label_')): for img in soup.findAll(src=re.compile('/label_')):
process_section(img) process_section(img)
return sections return sections
def parse_index(self): def parse_index(self):
soup = self.get_current_issue() soup = self.get_current_issue()
if not soup: if not soup:
@ -78,10 +82,10 @@ class Newsweek(BasicNewsRecipe):
if match is not None: if match is not None:
self.timefmt = strftime(' [%d %b, %Y]', time.strptime(match.group(1), '%y%m%d')) self.timefmt = strftime(' [%d %b, %Y]', time.strptime(match.group(1), '%y%m%d'))
self.cover_url = small.replace('coversmall', 'coverlarge') self.cover_url = small.replace('coversmall', 'coverlarge')
sections = self.get_sections(soup) sections = self.get_sections(soup)
sections.insert(0, ('Main articles', [])) sections.insert(0, ('Main articles', []))
for tag in soup.findAll('h5'): for tag in soup.findAll('h5'):
a = tag.find('a', href=True) a = tag.find('a', href=True)
if a is not None: if a is not None:
@ -97,8 +101,8 @@ class Newsweek(BasicNewsRecipe):
if art['title'] and art['url']: if art['title'] and art['url']:
sections[0][1].append(art) sections[0][1].append(art)
return sections return sections
def postprocess_html(self, soup, first_fetch): def postprocess_html(self, soup, first_fetch):
divs = list(soup.findAll('div', 'pagination')) divs = list(soup.findAll('div', 'pagination'))
if not divs: if not divs:
@ -106,8 +110,8 @@ class Newsweek(BasicNewsRecipe):
divs[0].extract() divs[0].extract()
if len(divs) > 1: if len(divs) > 1:
soup.find('body')['style'] = 'page-break-after:avoid' soup.find('body')['style'] = 'page-break-after:avoid'
divs[1].extract() divs[1].extract()
h1 = soup.find('h1') h1 = soup.find('h1')
if h1: if h1:
h1.extract() h1.extract()
@ -116,12 +120,12 @@ class Newsweek(BasicNewsRecipe):
else: else:
soup.find('body')['style'] = 'page-break-before:always; page-break-after:avoid;' soup.find('body')['style'] = 'page-break-before:always; page-break-after:avoid;'
return soup return soup
def get_current_issue(self): def get_current_issue(self):
#from urllib2 import urlopen # For some reason mechanize fails #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) soup = self.index_to_soup('http://www.newsweek.com')#BeautifulSoup(home)
img = soup.find('img', alt='Current Magazine') img = soup.find('img', alt='Current Magazine')
if img and img.parent.has_key('href'): if img and img.parent.has_key('href'):
return self.index_to_soup(img.parent['href']) return self.index_to_soup(img.parent['href'])

View File

@ -7,7 +7,7 @@ from calibre import preferred_encoding, strftime
class Template(MarkupTemplate): class Template(MarkupTemplate):
def generate(self, *args, **kwargs): def generate(self, *args, **kwargs):
if not kwargs.has_key('style'): if not kwargs.has_key('style'):
kwargs['style'] = '' kwargs['style'] = ''
@ -17,20 +17,20 @@ class Template(MarkupTemplate):
for arg in args: for arg in args:
if isinstance(arg, basestring) and not isinstance(arg, unicode): if isinstance(arg, basestring) and not isinstance(arg, unicode):
arg = unicode(arg, 'utf-8', 'replace') arg = unicode(arg, 'utf-8', 'replace')
return MarkupTemplate.generate(self, *args, **kwargs) return MarkupTemplate.generate(self, *args, **kwargs)
class NavBarTemplate(Template): class NavBarTemplate(Template):
def __init__(self): def __init__(self):
Template.__init__(self, u'''\ 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"> "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" xml:lang="en"
xmlns:xi="http://www.w3.org/2001/XInclude" xmlns:xi="http://www.w3.org/2001/XInclude"
xmlns:py="http://genshi.edgewall.org/" xmlns:py="http://genshi.edgewall.org/"
> >
<head> <head>
<style py:if="extra_css" type="text/css"> <style py:if="extra_css" type="text/css">
@ -38,7 +38,7 @@ class NavBarTemplate(Template):
</style> </style>
</head> </head>
<body> <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" /> <hr py:if="bottom" />
<p py:if="bottom" style="text-align:left"> <p py:if="bottom" style="text-align:left">
This article was downloaded by <b>${__appname__}</b> from <a href="${url}">${url}</a> 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"> <py:if test="art == num - 1 and not bottom">
| <a href="${prefix}../../feed_${str(feed+1)}/index.html">Next</a> | <a href="${prefix}../../feed_${str(feed+1)}/index.html">Next</a>
</py:if> </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"> <py:if test="two_levels">
| <a href="${prefix}../../index.html#feed_${str(feed)}">Main menu</a> | <a href="${prefix}../../index.html#feed_${str(feed)}">Main menu</a>
</py:if> </py:if>
@ -64,29 +64,29 @@ class NavBarTemplate(Template):
</html> </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, two_levels, url, __appname__, prefix='', center=True,
extra_css=None): extra_css=None):
if prefix and not prefix.endswith('/'): if prefix and not prefix.endswith('/'):
prefix += '/' prefix += '/'
return Template.generate(self, bottom=bottom, art=art, feed=feed, 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, two_levels=two_levels, url=url,
__appname__=__appname__, prefix=prefix, __appname__=__appname__, prefix=prefix,
center=center, extra_css=extra_css) center=center, extra_css=extra_css)
class IndexTemplate(Template): class IndexTemplate(Template):
def __init__(self): def __init__(self):
Template.__init__(self, u'''\ 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"> "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" xml:lang="en"
xmlns:xi="http://www.w3.org/2001/XInclude" xmlns:xi="http://www.w3.org/2001/XInclude"
xmlns:py="http://genshi.edgewall.org/" xmlns:py="http://genshi.edgewall.org/"
> >
<head> <head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
@ -99,15 +99,17 @@ class IndexTemplate(Template):
</style> </style>
</head> </head>
<body> <body>
<h1 class="calibre_recipe_title">${title}</h1> <div class="calibre_rescale_100">
<p style="text-align:right">${date}</p> <h1 class="calibre_recipe_title calibre_rescale_180">${title}</h1>
<ul class="calibre_feed_list"> <p style="text-align:right">${date}</p>
<py:for each="i, feed in enumerate(feeds)"> <ul class="calibre_feed_list">
<li py:if="feed" id="feed_${str(i)}"> <py:for each="i, feed in enumerate(feeds)">
<a class="feed" href="${'feed_%d/index.html'%i}">${feed.title}</a> <li py:if="feed" id="feed_${str(i)}">
</li> <a class="feed calibre_rescale_120" href="${'feed_%d/index.html'%i}">${feed.title}</a>
</py:for> </li>
</ul> </py:for>
</ul>
</div>
</body> </body>
</html> </html>
''') ''')
@ -118,19 +120,19 @@ class IndexTemplate(Template):
date = strftime(datefmt) date = strftime(datefmt)
return Template.generate(self, title=title, date=date, feeds=feeds, return Template.generate(self, title=title, date=date, feeds=feeds,
extra_css=extra_css) extra_css=extra_css)
class FeedTemplate(Template): class FeedTemplate(Template):
def __init__(self): def __init__(self):
Template.__init__(self, u'''\ 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"> "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" xml:lang="en"
xmlns:xi="http://www.w3.org/2001/XInclude" xmlns:xi="http://www.w3.org/2001/XInclude"
xmlns:py="http://genshi.edgewall.org/" xmlns:py="http://genshi.edgewall.org/"
> >
<head> <head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
@ -143,61 +145,64 @@ class FeedTemplate(Template):
</style> </style>
</head> </head>
<body style="page-break-before:always"> <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)"> <py:if test="getattr(feed, 'image', None)">
<div class="calibre_feed_image"> <div class="calibre_feed_image">
<img alt="${feed.image_alt}" src="${feed.image_url}" /> <img alt="${feed.image_alt}" src="${feed.image_url}" />
</div> </div>
</py:if> </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 /> ${feed.description}<br />
</div> </div>
<ul class="calibre_article_list"> <ul class="calibre_article_list">
<py:for each="i, article in enumerate(feed.articles)"> <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"> <li id="${'article_%d'%i}" py:if="getattr(article, 'downloaded',
<a class="article" href="${article.url}">${article.title}</a> 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> <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))} ${Markup(cutoff(article.text_summary))}
</div> </div>
</li> </li>
</py:for> </py:for>
</ul> </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> | | <a href="../index.html">Up one level</a> |
</div> </div>
</div>
</body> </body>
</html> </html>
''') ''')
def generate(self, feed, cutoff, extra_css=None): 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) extra_css=extra_css)
class EmbeddedContent(Template): class EmbeddedContent(Template):
def __init__(self): def __init__(self):
Template.__init__(self, u'''\ 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"> "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" xml:lang="en"
xmlns:xi="http://www.w3.org/2001/XInclude" xmlns:xi="http://www.w3.org/2001/XInclude"
xmlns:py="http://genshi.edgewall.org/" xmlns:py="http://genshi.edgewall.org/"
> >
<head> <head>
<title>${article.title}</title> <title>${article.title}</title>
</head> </head>
<body> <body>
<h2>${article.title}</h2> <h2>${article.title}</h2>
<div> <div>
${Markup(article.content if len(article.content if article.content else '') > len(article.summary if article.summary else '') else article.summary)} ${Markup(article.content if len(article.content if article.content else '') > len(article.summary if article.summary else '') else article.summary)}
</div> </div>
</body> </body>
</html> </html>
''') ''')
def generate(self, article): def generate(self, article):
return Template.generate(self, article=article) return Template.generate(self, article=article)

View File

@ -53,9 +53,15 @@ def save_soup(soup, target):
ns = BeautifulSoup('<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />') ns = BeautifulSoup('<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />')
nm = ns.find('meta') nm = ns.find('meta')
metas = soup.findAll('meta', content=True) metas = soup.findAll('meta', content=True)
added = False
for meta in metas: for meta in metas:
if 'charset' in meta.get('content', '').lower(): if 'charset' in meta.get('content', '').lower():
meta.replaceWith(nm) 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) selfdir = os.path.dirname(target)
@ -67,6 +73,7 @@ def save_soup(soup, target):
html = unicode(soup) html = unicode(soup)
with open(target, 'wb') as f: with open(target, 'wb') as f:
idx = html.find('hoping')
f.write(html.encode('utf-8')) f.write(html.encode('utf-8'))
class response(str): class response(str):