Merge from trunk

This commit is contained in:
Charles Haley 2011-04-08 15:44:37 +01:00
commit 8ac650cd4f
13 changed files with 378 additions and 46 deletions

62
recipes/al_ahram.recipe Normal file
View File

@ -0,0 +1,62 @@
# coding=utf-8
__license__ = 'GPL v3'
__copyright__ = '2011, Hassan Williamson <haz at hazrpg.co.uk>'
'''
ahram.org.eg
'''
from calibre.web.feeds.recipes import BasicNewsRecipe
class AlAhram(BasicNewsRecipe):
title = 'Al-Ahram'
__author__ = 'Hassan Williamson'
description = 'News from Egypt in Arabic.'
oldest_article = 7
max_articles_per_feed = 100
no_stylesheets = True
#delay = 1
use_embedded_content = False
encoding = 'utf8'
publisher = 'Al-Ahram'
category = 'News'
language = 'ar'
publication_type = 'newsportal'
extra_css = ' body{ font-family: Verdana,Helvetica,Arial,sans-serif; direction: rtl; } .txtTitle{ font-weight: bold; } '
keep_only_tags = [
dict(name='div', attrs={'class':['bbcolright']})
]
remove_tags = [
dict(name='div', attrs={'class':['bbnav', 'bbsp']}),
dict(name='div', attrs={'id':['AddThisButton']})
]
remove_attributes = [
'width','height'
]
feeds = [
(u'الأولى', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=25'),
(u'مصر', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=27'),
(u'المحافظات', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=29'),
(u'الوطن العربي', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=31'),
(u'العالم', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=26'),
(u'تقارير المراسلين', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=2'),
(u'تحقيقات', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=3'),
(u'قضايا واراء', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=4'),
(u'اقتصاد', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=5'),
(u'رياضة', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=6'),
(u'حوادث', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=38'),
(u'دنيا الثقافة', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=7'),
(u'المراة والطفل', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=8'),
(u'يوم جديد', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=9'),
(u'الكتاب', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=10'),
(u'الاعمدة', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=11'),
(u'أراء حرة', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=59'),
(u'ملفات الاهرام', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=12'),
(u'بريد الاهرام', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=15'),
(u'الاخيرة', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=16'),
]

View File

@ -18,7 +18,8 @@ class Economist(BasicNewsRecipe):
__author__ = "Kovid Goyal"
INDEX = 'http://www.economist.com/printedition'
description = 'Global news and current affairs from a European perspective.'
description = ('Global news and current affairs from a European'
' perspective. Best downloaded on Friday mornings (GMT)')
oldest_article = 7.0
cover_url = 'http://www.economist.com/images/covers/currentcoverus_large.jpg'

View File

@ -11,7 +11,8 @@ class Economist(BasicNewsRecipe):
language = 'en'
__author__ = "Kovid Goyal"
description = ('Global news and current affairs from a European perspective.'
description = ('Global news and current affairs from a European'
' perspective. Best downloaded on Friday mornings (GMT).'
' Much slower than the print edition based version.')
oldest_article = 7.0

View File

@ -11,7 +11,8 @@ from calibre.web.feeds.news import BasicNewsRecipe
class FinancialTimes(BasicNewsRecipe):
title = u'Financial Times'
__author__ = 'Darko Miletic and Sujata Raman'
description = 'Financial world news'
description = ('Financial world news. Available after 5AM '
'GMT, daily.')
oldest_article = 2
language = 'en'

View File

@ -217,6 +217,10 @@ class ISBNMerge(object):
for r in results:
ans.identifiers.update(r.identifiers)
# Cover URL
ans.has_cached_cover_url = bool([r for r in results if
getattr(r, 'has_cached_cover_url', False)])
# Merge any other fields with no special handling (random merge)
touched_fields = set()
for r in results:
@ -253,10 +257,10 @@ def identify(log, abort, # {{{
plugins = [p for p in metadata_plugins(['identify']) if p.is_configured()]
kwargs = {
'title': title,
'authors': authors,
'identifiers': identifiers,
'timeout': timeout,
'title': title,
'authors': authors,
'identifiers': identifiers,
'timeout': timeout,
}
log('Running identify query with parameters:')

View File

@ -97,6 +97,10 @@ class CSSSelector(etree.XPath):
def __init__(self, css, namespaces=XPNSMAP):
css = self.MIN_SPACE_RE.sub(r'\1', css)
if isinstance(css, unicode):
# Workaround for bug in lxml on windows/OS X that causes a massive
# memory leak with non ASCII selectors
css = css.encode('ascii', 'ignore').decode('ascii')
try:
path = css_to_xpath(css)
except UnicodeEncodeError: # Bug in css_to_xpath

View File

@ -145,11 +145,10 @@ class InterfaceAction(QObject):
ans[candidate] = zf.read(candidate)
return ans
def genesis(self):
'''
Setup this plugin. Only called once during initialization. self.gui is
available. The action secified by :attr:`action_spec` is available as
available. The action specified by :attr:`action_spec` is available as
``self.qaction``.
'''
pass

View File

@ -8,7 +8,7 @@ __docformat__ = 'restructuredtext en'
import os, shutil
from functools import partial
from PyQt4.Qt import QMenu, Qt, QInputDialog
from PyQt4.Qt import QMenu, Qt, QInputDialog, QToolButton
from calibre import isbytestring
from calibre.constants import filesystem_encoding
@ -88,6 +88,9 @@ class ChooseLibraryAction(InterfaceAction):
type=Qt.QueuedConnection)
self.stats = LibraryUsageStats()
self.popup_type = (QToolButton.InstantPopup if len(self.stats.stats) > 1 else
QToolButton.MenuButtonPopup)
self.create_action(spec=(_('Switch/create library...'), 'lt.png', None,
None), attr='action_choose')
self.action_choose.triggered.connect(self.choose_library,
@ -123,6 +126,7 @@ class ChooseLibraryAction(InterfaceAction):
type=Qt.QueuedConnection)
self.choose_menu.addAction(ac)
self.rename_separator = self.choose_menu.addSeparator()
self.maintenance_menu = QMenu(_('Library Maintenance'))
@ -172,6 +176,7 @@ class ChooseLibraryAction(InterfaceAction):
return
db = self.gui.library_view.model().db
locations = list(self.stats.locations(db))
for ac in self.switch_actions:
ac.setVisible(False)
self.quick_menu.clear()

View File

@ -141,15 +141,18 @@ class EditMetadataAction(InterfaceAction):
list(range(self.gui.library_view.model().rowCount(QModelIndex())))
current_row = row_list.index(cr)
if test_eight_code:
changed = self.do_edit_metadata(row_list, current_row)
else:
changed = self.do_edit_metadata_old(row_list, current_row)
func = (self.do_edit_metadata if test_eight_code else
self.do_edit_metadata_old)
changed, rows_to_refresh = func(row_list, current_row)
m = self.gui.library_view.model()
if rows_to_refresh:
m.refresh_rows(rows_to_refresh)
if changed:
self.gui.library_view.model().refresh_ids(list(changed))
m.refresh_ids(list(changed))
current = self.gui.library_view.currentIndex()
m = self.gui.library_view.model()
if self.gui.cover_flow:
self.gui.cover_flow.dataChanged()
m.current_changed(current, previous)
@ -183,6 +186,7 @@ class EditMetadataAction(InterfaceAction):
current_row += d.row_delta
self.gui.library_view.set_current_row(current_row)
self.gui.library_view.scroll_to_row(current_row)
return changed, set()
def do_edit_metadata(self, row_list, current_row):
from calibre.gui2.metadata.single import edit_metadata
@ -190,7 +194,7 @@ class EditMetadataAction(InterfaceAction):
changed, rows_to_refresh = edit_metadata(db, row_list, current_row,
parent=self.gui, view_slot=self.view_format_callback,
set_current_callback=self.set_current_callback)
return changed
return changed, rows_to_refresh
def set_current_callback(self, id_):
db = self.gui.library_view.model().db

View File

@ -7,9 +7,9 @@ __docformat__ = 'restructuredtext en'
from functools import partial
from PyQt4.Qt import QIcon, Qt, QWidget, QToolBar, QSize, \
pyqtSignal, QToolButton, QMenu, \
QObject, QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout, QActionGroup
from PyQt4.Qt import (QIcon, Qt, QWidget, QToolBar, QSize,
pyqtSignal, QToolButton, QMenu,
QObject, QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout, QActionGroup)
from calibre.constants import __appname__
@ -264,11 +264,11 @@ class ToolBar(QToolBar): # {{{
def apply_settings(self):
sz = gprefs['toolbar_icon_size']
sz = {'small':24, 'medium':48, 'large':64}[sz]
sz = {'off':0, 'small':24, 'medium':48, 'large':64}[sz]
self.setIconSize(QSize(sz, sz))
self.child_bar.setIconSize(QSize(sz, sz))
style = Qt.ToolButtonTextUnderIcon
if gprefs['toolbar_text'] == 'never':
if sz > 0 and gprefs['toolbar_text'] == 'never':
style = Qt.ToolButtonIconOnly
self.setToolButtonStyle(style)
self.child_bar.setToolButtonStyle(style)

View File

@ -8,26 +8,22 @@ __copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from threading import Thread, Event
from operator import attrgetter
from PyQt4.Qt import (QStyledItemDelegate, QTextDocument, QRectF, QIcon, Qt,
QStyle, QApplication, QDialog, QVBoxLayout, QLabel, QDialogButtonBox,
QStackedWidget, QWidget, QTableView, QGridLayout, QFontInfo, QPalette)
QStackedWidget, QWidget, QTableView, QGridLayout, QFontInfo, QPalette,
QTimer, pyqtSignal, QAbstractTableModel, QVariant, QSize)
from PyQt4.QtWebKit import QWebView
from calibre.customize.ui import metadata_plugins
from calibre.ebooks.metadata import authors_to_string
from calibre.utils.logging import ThreadSafeLog, UnicodeHTMLStream
from calibre.utils.logging import GUILog as Log
from calibre.ebooks.metadata.sources.identify import identify
class Log(ThreadSafeLog): # {{{
def __init__(self):
ThreadSafeLog.__init__(self, level=self.DEBUG)
self.outputs = [UnicodeHTMLStream()]
def clear(self):
self.outputs[0].clear()
# }}}
from calibre.ebooks.metadata.book.base import Metadata
from calibre.gui2 import error_dialog, NONE
from calibre.utils.date import utcnow, fromordinal, format_date
from calibre.library.comments import comments_to_html
class RichTextDelegate(QStyledItemDelegate): # {{{
@ -56,18 +52,149 @@ class RichTextDelegate(QStyledItemDelegate): # {{{
painter.restore()
# }}}
class ResultsView(QTableView):
class ResultsModel(QAbstractTableModel): # {{{
COLUMNS = (
'#', _('Title'), _('Published'), _('Has cover'), _('Has summary')
)
HTML_COLS = (1, 2)
ICON_COLS = (3, 4)
def __init__(self, results, parent=None):
QAbstractTableModel.__init__(self, parent)
self.results = results
self.yes_icon = QVariant(QIcon(I('ok.png')))
def rowCount(self, parent=None):
return len(self.results)
def columnCount(self, parent=None):
return len(self.COLUMNS)
def headerData(self, section, orientation, role):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
try:
return QVariant(self.COLUMNS[section])
except:
return NONE
return NONE
def data_as_text(self, book, col):
if col == 0:
return unicode(book.gui_rank+1)
if col == 1:
t = book.title if book.title else _('Unknown')
a = authors_to_string(book.authors) if book.authors else ''
return '<b>%s</b><br><i>%s</i>' % (t, a)
if col == 2:
d = format_date(book.pubdate, 'yyyy') if book.pubdate else _('Unknown')
p = book.publisher if book.publisher else ''
return '<b>%s</b><br><i>%s</i>' % (d, p)
def data(self, index, role):
row, col = index.row(), index.column()
try:
book = self.results[row]
except:
return NONE
if role == Qt.DisplayRole and col not in self.ICON_COLS:
res = self.data_as_text(book, col)
if res:
return QVariant(res)
return NONE
elif role == Qt.DecorationRole and col in self.ICON_COLS:
if col == 3 and getattr(book, 'has_cached_cover_url', False):
return self.yes_icon
if col == 4 and book.comments:
return self.yes_icon
elif role == Qt.UserRole:
return book
return NONE
def sort(self, col, order=Qt.AscendingOrder):
key = lambda x: x
if col == 0:
key = attrgetter('gui_rank')
elif col == 1:
key = attrgetter('title')
elif col == 2:
key = attrgetter('authors')
elif col == 3:
key = attrgetter('has_cached_cover_url')
elif key == 4:
key = lambda x: bool(x.comments)
self.results.sort(key=key, reverse=order==Qt.AscendingOrder)
self.reset()
# }}}
class ResultsView(QTableView): # {{{
show_details_signal = pyqtSignal(object)
book_selected = pyqtSignal(object)
def __init__(self, parent=None):
QTableView.__init__(self, parent)
self.rt_delegate = RichTextDelegate(self)
self.setSelectionMode(self.SingleSelection)
self.setAlternatingRowColors(True)
self.setSelectionBehavior(self.SelectRows)
self.setIconSize(QSize(24, 24))
self.clicked.connect(self.show_details)
self.doubleClicked.connect(self.select_index)
self.setSortingEnabled(True)
def show_results(self, results):
self._model = ResultsModel(results, self)
self.setModel(self._model)
for i in self._model.HTML_COLS:
self.setItemDelegateForColumn(i, self.rt_delegate)
self.resizeRowsToContents()
self.resizeColumnsToContents()
self.setFocus(Qt.OtherFocusReason)
def currentChanged(self, current, previous):
ret = QTableView.currentChanged(self, current, previous)
self.show_details(current)
return ret
def show_details(self, index):
book = self.model().data(index, Qt.UserRole)
parts = [
'<center>',
'<h2>%s</h2>'%book.title,
'<div><i>%s</i></div>'%authors_to_string(book.authors),
]
if not book.is_null('rating'):
parts.append('<div>%s</div>'%('\u2605'*int(book.rating)))
parts.append('</center>')
if book.tags:
parts.append('<div>%s</div><div>\u00a0</div>'%', '.join(book.tags))
if book.comments:
parts.append(comments_to_html(book.comments))
self.show_details_signal.emit(''.join(parts))
def select_index(self, index):
if not index.isValid():
index = self.model().index(0, 0)
book = self.model().data(index, Qt.UserRole)
self.book_selected.emit(book)
def get_result(self):
self.select_index(self.currentIndex())
# }}}
class Comments(QWebView): # {{{
def __init__(self, parent=None):
QWebView.__init__(self, parent)
self.setAcceptDrops(False)
self.setMaximumWidth(270)
self.setMinimumWidth(270)
self.setMaximumWidth(300)
self.setMinimumWidth(300)
palette = self.palette()
palette.setBrush(QPalette.Base, Qt.transparent)
@ -109,7 +236,7 @@ class Comments(QWebView): # {{{
self.setHtml(templ%html)
# }}}
class IdentifyWorker(Thread):
class IdentifyWorker(Thread): # {{{
def __init__(self, log, abort, title, authors, identifiers):
Thread.__init__(self)
@ -122,17 +249,42 @@ class IdentifyWorker(Thread):
self.results = []
self.error = None
def sample_results(self):
m1 = Metadata('The Great Gatsby', ['Francis Scott Fitzgerald'])
m2 = Metadata('The Great Gatsby', ['F. Scott Fitzgerald'])
m1.has_cached_cover_url = True
m2.has_cached_cover_url = False
m1.comments = 'Some comments '*10
m1.tags = ['tag%d'%i for i in range(20)]
m1.rating = 4.4
m1.language = 'en'
m2.language = 'fr'
m1.pubdate = utcnow()
m2.pubdate = fromordinal(1000000)
m1.publisher = 'Publisher 1'
m2.publisher = 'Publisher 2'
return [m1, m2]
def run(self):
try:
self.results = identify(self.log, self.abort, title=self.title,
authors=self.authors, identifiers=self.identifiers)
if True:
self.results = self.sample_results()
else:
self.results = identify(self.log, self.abort, title=self.title,
authors=self.authors, identifiers=self.identifiers)
for i, result in enumerate(self.results):
result.gui_rank = i
except:
import traceback
self.error = traceback.format_exc()
# }}}
class IdentifyWidget(QWidget):
class IdentifyWidget(QWidget): # {{{
rejected = pyqtSignal()
results_found = pyqtSignal()
book_selected = pyqtSignal(object)
def __init__(self, log, parent=None):
QWidget.__init__(self, parent)
@ -150,11 +302,15 @@ class IdentifyWidget(QWidget):
l.addWidget(self.top, 0, 0)
self.results_view = ResultsView(self)
self.results_view.book_selected.connect(self.book_selected.emit)
self.get_result = self.results_view.get_result
l.addWidget(self.results_view, 1, 0)
self.comments_view = Comments(self)
l.addWidget(self.comments_view, 1, 1)
self.results_view.show_details_signal.connect(self.comments_view.show_data)
self.query = QLabel('download starting...')
f = self.query.font()
f.setPointSize(f.pointSize()-2)
@ -197,7 +353,50 @@ class IdentifyWidget(QWidget):
self.worker = IdentifyWorker(self.log, self.abort, title,
authors, identifiers)
# self.worker.start()
self.worker.start()
QTimer.singleShot(50, self.update)
def update(self):
if self.worker.is_alive():
QTimer.singleShot(50, self.update)
else:
self.process_results()
def process_results(self):
if self.worker.error is not None:
error_dialog(self, _('Download failed'),
_('Failed to download metadata. Click '
'Show Details to see details'),
show=True, det_msg=self.worker.error)
self.rejected.emit()
return
if not self.worker.results:
log = ''.join(self.log.plain_text)
error_dialog(self, _('No matches found'), '<p>' +
_('Failed to find any books that '
'match your search. Try making the search <b>less '
'specific</b>. For example, use only the author\'s '
'last name and a single distinctive word from '
'the title.<p>To see the full log, click Show Details.'),
show=True, det_msg=log)
self.rejected.emit()
return
self.results_view.show_results(self.worker.results)
self.comments_view.show_data('''
<div style="margin-bottom:2ex">Found <b>%d</b> results</div>
<div>To see <b>details</b>, click on any result</div>''' %
len(self.worker.results))
self.results_found.emit()
def cancel(self):
self.abort.set()
# }}}
class FullFetch(QDialog): # {{{
@ -213,16 +412,44 @@ class FullFetch(QDialog): # {{{
self.setLayout(l)
l.addWidget(self.stack)
self.bb = QDialogButtonBox(QDialogButtonBox.Cancel)
self.bb = QDialogButtonBox(QDialogButtonBox.Cancel|QDialogButtonBox.Ok)
l.addWidget(self.bb)
self.bb.rejected.connect(self.reject)
self.next_button = self.bb.addButton(_('Next'), self.bb.AcceptRole)
self.next_button.setDefault(True)
self.next_button.setEnabled(False)
self.next_button.clicked.connect(self.next_clicked)
self.ok_button = self.bb.button(self.bb.Ok)
self.ok_button.setVisible(False)
self.ok_button.clicked.connect(self.ok_clicked)
self.identify_widget = IdentifyWidget(log, self)
self.identify_widget.rejected.connect(self.reject)
self.identify_widget.results_found.connect(self.identify_results_found)
self.identify_widget.book_selected.connect(self.book_selected)
self.stack.addWidget(self.identify_widget)
self.resize(850, 500)
def book_selected(self, book):
print (book)
self.next_button.setVisible(False)
self.ok_button.setVisible(True)
def accept(self):
# Prevent pressing Enter from closing the dialog
# Prevent the usual dialog accept mechanisms from working
pass
def reject(self):
self.identify_widget.cancel()
return QDialog.reject(self)
def identify_results_found(self):
self.next_button.setEnabled(True)
def next_clicked(self, *args):
self.identify_widget.get_result()
def ok_clicked(self, *args):
pass
def start(self, title=None, authors=None, identifiers={}):

View File

@ -49,8 +49,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
r('use_roman_numerals_for_series_number', config)
r('separate_cover_flow', config, restart_required=True)
choices = [(_('Small'), 'small'), (_('Medium'), 'medium'),
(_('Large'), 'large')]
choices = [(_('Off'), 'off'), (_('Small'), 'small'),
(_('Medium'), 'medium'), (_('Large'), 'large')]
r('toolbar_icon_size', gprefs, choices=choices)
choices = [(_('Automatic'), 'auto'), (_('Always'), 'always'),

View File

@ -108,10 +108,13 @@ class UnicodeHTMLStream(HTMLStream):
elif not isinstance(arg, unicode):
arg = as_unicode(arg)
self.data.append(arg+sep)
self.plain_text.append(arg+sep)
self.data.append(end)
self.plain_text.append(end)
def clear(self):
self.data = []
self.plain_text = []
self.last_col = self.color[INFO]
@property
@ -162,4 +165,25 @@ class ThreadSafeLog(Log):
with self._lock:
Log.prints(self, *args, **kwargs)
class GUILog(ThreadSafeLog):
'''
Logs in HTML and plain text as unicode. Ideal for display in a GUI context.
'''
def __init__(self):
ThreadSafeLog.__init__(self, level=self.DEBUG)
self.outputs = [UnicodeHTMLStream()]
def clear(self):
self.outputs[0].clear()
@property
def html(self):
return self.outputs[0].html
@property
def plain_text(self):
return u''.join(self.outputs[0].plain_text)
default_log = Log()