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):
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()

View File

@ -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):

View File

@ -290,14 +290,6 @@ class MobiReader(object):
self.replace_page_breaks()
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...')
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 <p> 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 <html> tag')
nroot = html.fromstring('<html><head></head><body></body></html>')
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 <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)
guides = root.xpath('//guide')
guide = guides[0] if guides else None

View File

@ -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)

View File

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

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -147,19 +147,6 @@
<item>
<layout class="QGridLayout" name="gridLayout_2">
<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">
<property name="text">
<string>Default network &amp;timeout:</string>
@ -169,7 +156,7 @@
</property>
</widget>
</item>
<item row="2" column="1">
<item row="1" column="1">
<widget class="QSpinBox" name="timeout">
<property name="toolTip">
<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>
</widget>
</item>
<item row="3" column="1">
<item row="2" column="1">
<widget class="QComboBox" name="language"/>
</item>
<item row="3" column="0">
<item row="2" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Choose &amp;language (requires restart):</string>
@ -201,7 +188,7 @@
</property>
</widget>
</item>
<item row="4" column="1">
<item row="3" column="1">
<widget class="QComboBox" name="priority">
<item>
<property name="text">
@ -220,7 +207,7 @@
</item>
</widget>
</item>
<item row="4" column="0">
<item row="3" column="0">
<widget class="QLabel" name="priority_label">
<property name="text">
<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.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)

View File

@ -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 = _('<b>Failed to download metadata:')+\
'</b><br><pre>'+x.tb+'</pre>'
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):

View File

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

View File

@ -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 = '<p><b>' + unicode(str(value), 'utf8', 'replace') + '</b></p>'
msg += '<p>Detailed <b>traceback</b>:<pre>'+fe+'</pre>'
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

View File

@ -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]

View File

@ -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'), _('<p>This book is protected by <a href="%s">DRM</a>')%'http://wiki.mobileread.com/wiki/DRM').exec_()
else:
ConversionErrorDialog(self, _('Could not open ebook'),
_('<b>%s</b><br/><p>%s</p>')%(worker.exception, worker.traceback.replace('\n', '<br>')), 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)

View File

@ -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<title>.+) - (?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))

View File

@ -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([

View File

@ -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):

View File

@ -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
return soup

View File

@ -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'])

View File

@ -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}</title>
</head>
<body>
<h2>${article.title}</h2>
<div>
${Markup(article.content if len(article.content if article.content else '') > len(article.summary if article.summary else '') else article.summary)}
</div>
</body>
</html>
</html>
''')
def generate(self, 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" />')
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):