mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-07 10:14:46 -04:00
Merge from main branch
This commit is contained in:
commit
8ddd3ebeca
@ -18,7 +18,8 @@ class Economist(BasicNewsRecipe):
|
|||||||
|
|
||||||
__author__ = "Kovid Goyal"
|
__author__ = "Kovid Goyal"
|
||||||
INDEX = 'http://www.economist.com/printedition'
|
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
|
oldest_article = 7.0
|
||||||
cover_url = 'http://www.economist.com/images/covers/currentcoverus_large.jpg'
|
cover_url = 'http://www.economist.com/images/covers/currentcoverus_large.jpg'
|
||||||
|
@ -11,7 +11,8 @@ class Economist(BasicNewsRecipe):
|
|||||||
language = 'en'
|
language = 'en'
|
||||||
|
|
||||||
__author__ = "Kovid Goyal"
|
__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.')
|
' Much slower than the print edition based version.')
|
||||||
|
|
||||||
oldest_article = 7.0
|
oldest_article = 7.0
|
||||||
|
@ -11,7 +11,8 @@ from calibre.web.feeds.news import BasicNewsRecipe
|
|||||||
class FinancialTimes(BasicNewsRecipe):
|
class FinancialTimes(BasicNewsRecipe):
|
||||||
title = u'Financial Times'
|
title = u'Financial Times'
|
||||||
__author__ = 'Darko Miletic and Sujata Raman'
|
__author__ = 'Darko Miletic and Sujata Raman'
|
||||||
description = 'Financial world news'
|
description = ('Financial world news. Available after 5AM '
|
||||||
|
'GMT, daily.')
|
||||||
oldest_article = 2
|
oldest_article = 2
|
||||||
language = 'en'
|
language = 'en'
|
||||||
|
|
||||||
|
@ -279,7 +279,7 @@ class Worker(Thread): # Get details {{{
|
|||||||
|
|
||||||
class Amazon(Source):
|
class Amazon(Source):
|
||||||
|
|
||||||
name = 'Amazon Metadata'
|
name = 'Amazon Store'
|
||||||
description = _('Downloads metadata from Amazon')
|
description = _('Downloads metadata from Amazon')
|
||||||
|
|
||||||
capabilities = frozenset(['identify', 'cover'])
|
capabilities = frozenset(['identify', 'cover'])
|
||||||
|
@ -167,6 +167,13 @@ class Source(Plugin):
|
|||||||
|
|
||||||
# Configuration {{{
|
# Configuration {{{
|
||||||
|
|
||||||
|
def is_configured(self):
|
||||||
|
'''
|
||||||
|
Return False if your plugin needs to be configured before it can be
|
||||||
|
used. For example, it might need a username/password/API key.
|
||||||
|
'''
|
||||||
|
return True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def prefs(self):
|
def prefs(self):
|
||||||
if self._config_obj is None:
|
if self._config_obj is None:
|
||||||
|
@ -76,7 +76,7 @@ def run_download(log, results, abort,
|
|||||||
(plugin, width, height, fmt, bytes)
|
(plugin, width, height, fmt, bytes)
|
||||||
|
|
||||||
'''
|
'''
|
||||||
plugins = list(metadata_plugins(['cover']))
|
plugins = [p for p in metadata_plugins(['cover']) if p.is_configured()]
|
||||||
|
|
||||||
rq = Queue()
|
rq = Queue()
|
||||||
workers = [Worker(p, abort, title, authors, identifiers, timeout, rq) for p
|
workers = [Worker(p, abort, title, authors, identifiers, timeout, rq) for p
|
||||||
|
@ -217,6 +217,10 @@ class ISBNMerge(object):
|
|||||||
for r in results:
|
for r in results:
|
||||||
ans.identifiers.update(r.identifiers)
|
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)
|
# Merge any other fields with no special handling (random merge)
|
||||||
touched_fields = set()
|
touched_fields = set()
|
||||||
for r in results:
|
for r in results:
|
||||||
@ -250,13 +254,13 @@ def merge_identify_results(result_map, log):
|
|||||||
def identify(log, abort, # {{{
|
def identify(log, abort, # {{{
|
||||||
title=None, authors=None, identifiers={}, timeout=30):
|
title=None, authors=None, identifiers={}, timeout=30):
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
plugins = list(metadata_plugins(['identify']))
|
plugins = [p for p in metadata_plugins(['identify']) if p.is_configured()]
|
||||||
|
|
||||||
kwargs = {
|
kwargs = {
|
||||||
'title': title,
|
'title': title,
|
||||||
'authors': authors,
|
'authors': authors,
|
||||||
'identifiers': identifiers,
|
'identifiers': identifiers,
|
||||||
'timeout': timeout,
|
'timeout': timeout,
|
||||||
}
|
}
|
||||||
|
|
||||||
log('Running identify query with parameters:')
|
log('Running identify query with parameters:')
|
||||||
|
@ -37,4 +37,7 @@ class ISBNDB(Source):
|
|||||||
|
|
||||||
self.isbndb_key = prefs['isbndb_key']
|
self.isbndb_key = prefs['isbndb_key']
|
||||||
|
|
||||||
|
def is_configured(self):
|
||||||
|
return self.isbndb_key is not None
|
||||||
|
|
||||||
|
|
||||||
|
@ -264,11 +264,11 @@ class ToolBar(QToolBar): # {{{
|
|||||||
|
|
||||||
def apply_settings(self):
|
def apply_settings(self):
|
||||||
sz = gprefs['toolbar_icon_size']
|
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.setIconSize(QSize(sz, sz))
|
||||||
self.child_bar.setIconSize(QSize(sz, sz))
|
self.child_bar.setIconSize(QSize(sz, sz))
|
||||||
style = Qt.ToolButtonTextUnderIcon
|
style = Qt.ToolButtonTextUnderIcon
|
||||||
if gprefs['toolbar_text'] == 'never':
|
if sz > 0 and gprefs['toolbar_text'] == 'never':
|
||||||
style = Qt.ToolButtonIconOnly
|
style = Qt.ToolButtonIconOnly
|
||||||
self.setToolButtonStyle(style)
|
self.setToolButtonStyle(style)
|
||||||
self.child_bar.setToolButtonStyle(style)
|
self.child_bar.setToolButtonStyle(style)
|
||||||
|
@ -7,8 +7,22 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
from PyQt4.Qt import (QStyledItemDelegate, QTextDocument, QRectF,
|
from threading import Thread, Event
|
||||||
QStyle, QApplication)
|
|
||||||
|
from PyQt4.Qt import (QStyledItemDelegate, QTextDocument, QRectF, QIcon, Qt,
|
||||||
|
QStyle, QApplication, QDialog, QVBoxLayout, QLabel, QDialogButtonBox,
|
||||||
|
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 GUILog as Log
|
||||||
|
from calibre.ebooks.metadata.sources.identify import identify
|
||||||
|
from calibre.ebooks.metadata.book.base import Metadata
|
||||||
|
from calibre.gui2 import error_dialog, NONE
|
||||||
|
from calibre.utils.date import utcnow, fromordinal, format_date
|
||||||
|
|
||||||
|
|
||||||
class RichTextDelegate(QStyledItemDelegate): # {{{
|
class RichTextDelegate(QStyledItemDelegate): # {{{
|
||||||
|
|
||||||
@ -37,3 +51,323 @@ class RichTextDelegate(QStyledItemDelegate): # {{{
|
|||||||
painter.restore()
|
painter.restore()
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
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 role != Qt.DisplayRole:
|
||||||
|
return NONE
|
||||||
|
if orientation == Qt.Horizontal:
|
||||||
|
try:
|
||||||
|
return QVariant(self.COLUMNS[section])
|
||||||
|
except:
|
||||||
|
return NONE
|
||||||
|
else:
|
||||||
|
return QVariant(unicode(section+1))
|
||||||
|
|
||||||
|
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
|
||||||
|
return NONE
|
||||||
|
|
||||||
|
class ResultsView(QTableView): # {{{
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class Comments(QWebView): # {{{
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
QWebView.__init__(self, parent)
|
||||||
|
self.setAcceptDrops(False)
|
||||||
|
self.setMaximumWidth(300)
|
||||||
|
self.setMinimumWidth(300)
|
||||||
|
|
||||||
|
palette = self.palette()
|
||||||
|
palette.setBrush(QPalette.Base, Qt.transparent)
|
||||||
|
self.page().setPalette(palette)
|
||||||
|
self.setAttribute(Qt.WA_OpaquePaintEvent, False)
|
||||||
|
|
||||||
|
def turnoff_scrollbar(self, *args):
|
||||||
|
self.page().mainFrame().setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff)
|
||||||
|
|
||||||
|
def show_data(self, html):
|
||||||
|
def color_to_string(col):
|
||||||
|
ans = '#000000'
|
||||||
|
if col.isValid():
|
||||||
|
col = col.toRgb()
|
||||||
|
if col.isValid():
|
||||||
|
ans = unicode(col.name())
|
||||||
|
return ans
|
||||||
|
|
||||||
|
f = QFontInfo(QApplication.font(self.parent())).pixelSize()
|
||||||
|
c = color_to_string(QApplication.palette().color(QPalette.Normal,
|
||||||
|
QPalette.WindowText))
|
||||||
|
templ = '''\
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style type="text/css">
|
||||||
|
body, td {background-color: transparent; font-size: %dpx; color: %s }
|
||||||
|
a { text-decoration: none; color: blue }
|
||||||
|
div.description { margin-top: 0; padding-top: 0; text-indent: 0 }
|
||||||
|
table { margin-bottom: 0; padding-bottom: 0; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="description">
|
||||||
|
%%s
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
<html>
|
||||||
|
'''%(f, c)
|
||||||
|
self.setHtml(templ%html)
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class IdentifyWorker(Thread): # {{{
|
||||||
|
|
||||||
|
def __init__(self, log, abort, title, authors, identifiers):
|
||||||
|
Thread.__init__(self)
|
||||||
|
self.daemon = True
|
||||||
|
|
||||||
|
self.log, self.abort = log, abort
|
||||||
|
self.title, self.authors, self.identifiers = (title, authors,
|
||||||
|
identifiers)
|
||||||
|
|
||||||
|
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:
|
||||||
|
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): # {{{
|
||||||
|
|
||||||
|
rejected = pyqtSignal()
|
||||||
|
|
||||||
|
def __init__(self, log, parent=None):
|
||||||
|
QWidget.__init__(self, parent)
|
||||||
|
self.log = log
|
||||||
|
self.abort = Event()
|
||||||
|
|
||||||
|
self.l = l = QGridLayout()
|
||||||
|
self.setLayout(l)
|
||||||
|
|
||||||
|
names = ['<b>'+p.name+'</b>' for p in metadata_plugins(['identify']) if
|
||||||
|
p.is_configured()]
|
||||||
|
self.top = QLabel('<p>'+_('calibre is downloading metadata from: ') +
|
||||||
|
', '.join(names))
|
||||||
|
self.top.setWordWrap(True)
|
||||||
|
l.addWidget(self.top, 0, 0)
|
||||||
|
|
||||||
|
self.results_view = ResultsView(self)
|
||||||
|
l.addWidget(self.results_view, 1, 0)
|
||||||
|
|
||||||
|
self.comments_view = Comments(self)
|
||||||
|
l.addWidget(self.comments_view, 1, 1)
|
||||||
|
|
||||||
|
self.query = QLabel('download starting...')
|
||||||
|
f = self.query.font()
|
||||||
|
f.setPointSize(f.pointSize()-2)
|
||||||
|
self.query.setFont(f)
|
||||||
|
self.query.setWordWrap(True)
|
||||||
|
l.addWidget(self.query, 2, 0, 1, 2)
|
||||||
|
|
||||||
|
self.comments_view.show_data('<h2>'+_('Downloading')+
|
||||||
|
'<br><span id="dots">.</span></h2>'+
|
||||||
|
'''
|
||||||
|
<script type="text/javascript">
|
||||||
|
window.onload=function(){
|
||||||
|
var dotspan = document.getElementById('dots');
|
||||||
|
window.setInterval(function(){
|
||||||
|
if(dotspan.textContent == '............'){
|
||||||
|
dotspan.textContent = '.';
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
dotspan.textContent += '.';
|
||||||
|
}
|
||||||
|
}, 400);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
''')
|
||||||
|
|
||||||
|
def start(self, title=None, authors=None, identifiers={}):
|
||||||
|
self.log.clear()
|
||||||
|
self.log('Starting download')
|
||||||
|
parts = []
|
||||||
|
if title:
|
||||||
|
parts.append('title:'+title)
|
||||||
|
if authors:
|
||||||
|
parts.append('authors:'+authors_to_string(authors))
|
||||||
|
if identifiers:
|
||||||
|
x = ', '.join('%s:%s'%(k, v) for k, v in identifiers)
|
||||||
|
parts.append(x)
|
||||||
|
self.query.setText(_('Query: ')+'; '.join(parts))
|
||||||
|
self.log(unicode(self.query.text()))
|
||||||
|
|
||||||
|
self.worker = IdentifyWorker(self.log, self.abort, title,
|
||||||
|
authors, identifiers)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
def cancel(self):
|
||||||
|
self.abort.set()
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class FullFetch(QDialog): # {{{
|
||||||
|
|
||||||
|
def __init__(self, log, parent=None):
|
||||||
|
QDialog.__init__(self, parent)
|
||||||
|
self.log = log
|
||||||
|
|
||||||
|
self.setWindowTitle(_('Downloading metadata...'))
|
||||||
|
self.setWindowIcon(QIcon(I('metadata.png')))
|
||||||
|
|
||||||
|
self.stack = QStackedWidget()
|
||||||
|
self.l = l = QVBoxLayout()
|
||||||
|
self.setLayout(l)
|
||||||
|
l.addWidget(self.stack)
|
||||||
|
|
||||||
|
self.bb = QDialogButtonBox(QDialogButtonBox.Cancel)
|
||||||
|
l.addWidget(self.bb)
|
||||||
|
self.bb.rejected.connect(self.reject)
|
||||||
|
|
||||||
|
self.identify_widget = IdentifyWidget(log, self)
|
||||||
|
self.identify_widget.rejected.connect(self.reject)
|
||||||
|
self.stack.addWidget(self.identify_widget)
|
||||||
|
self.resize(850, 500)
|
||||||
|
|
||||||
|
def accept(self):
|
||||||
|
# Prevent pressing Enter from closing the dialog
|
||||||
|
pass
|
||||||
|
|
||||||
|
def reject(self):
|
||||||
|
self.identify_widget.cancel()
|
||||||
|
return QDialog.reject(self)
|
||||||
|
|
||||||
|
def start(self, title=None, authors=None, identifiers={}):
|
||||||
|
self.identify_widget.start(title=title, authors=authors,
|
||||||
|
identifiers=identifiers)
|
||||||
|
self.exec_()
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app = QApplication([])
|
||||||
|
d = FullFetch(Log())
|
||||||
|
d.start(title='great gatsby', authors=['Fitzgerald'])
|
||||||
|
|
||||||
|
@ -49,8 +49,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
|||||||
r('use_roman_numerals_for_series_number', config)
|
r('use_roman_numerals_for_series_number', config)
|
||||||
r('separate_cover_flow', config, restart_required=True)
|
r('separate_cover_flow', config, restart_required=True)
|
||||||
|
|
||||||
choices = [(_('Small'), 'small'), (_('Medium'), 'medium'),
|
choices = [(_('Off'), 'off'), (_('Small'), 'small'),
|
||||||
(_('Large'), 'large')]
|
(_('Medium'), 'medium'), (_('Large'), 'large')]
|
||||||
r('toolbar_icon_size', gprefs, choices=choices)
|
r('toolbar_icon_size', gprefs, choices=choices)
|
||||||
|
|
||||||
choices = [(_('Automatic'), 'auto'), (_('Always'), 'always'),
|
choices = [(_('Automatic'), 'auto'), (_('Always'), 'always'),
|
||||||
|
@ -18,6 +18,7 @@ from calibre.utils.config import ConfigProxy
|
|||||||
from calibre.gui2 import error_dialog, config, open_url, warning_dialog, \
|
from calibre.gui2 import error_dialog, config, open_url, warning_dialog, \
|
||||||
Dispatcher, info_dialog
|
Dispatcher, info_dialog
|
||||||
from calibre import as_unicode
|
from calibre import as_unicode
|
||||||
|
from calibre.utils.icu import sort_key
|
||||||
|
|
||||||
class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||||
|
|
||||||
@ -42,8 +43,11 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
|||||||
else self.opt_password.Password))
|
else self.opt_password.Password))
|
||||||
self.opt_password.setEchoMode(self.opt_password.Password)
|
self.opt_password.setEchoMode(self.opt_password.Password)
|
||||||
|
|
||||||
restrictions = sorted(saved_searches().names(),
|
restrictions = sorted(saved_searches().names(), key=sort_key)
|
||||||
cmp=lambda x,y: cmp(x.lower(), y.lower()))
|
# verify that the current restriction still exists. If not, clear it.
|
||||||
|
csr = db.prefs.get('cs_restriction', None)
|
||||||
|
if csr and csr not in restrictions:
|
||||||
|
db.prefs.set('cs_restriction', '')
|
||||||
choices = [('', '')] + [(x, x) for x in restrictions]
|
choices = [('', '')] + [(x, x) for x in restrictions]
|
||||||
r('cs_restriction', db.prefs, choices=choices)
|
r('cs_restriction', db.prefs, choices=choices)
|
||||||
|
|
||||||
@ -57,17 +61,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
|||||||
|
|
||||||
r('autolaunch_server', config)
|
r('autolaunch_server', config)
|
||||||
|
|
||||||
def set_server_options(self):
|
|
||||||
c = self.proxy
|
|
||||||
c.set('port', self.opt_port.value())
|
|
||||||
c.set('username', unicode(self.opt_username.text()).strip())
|
|
||||||
p = unicode(self.opt_password.text()).strip()
|
|
||||||
if not p:
|
|
||||||
p = None
|
|
||||||
c.set('password', p)
|
|
||||||
|
|
||||||
def start_server(self):
|
def start_server(self):
|
||||||
self.set_server_options()
|
ConfigWidgetBase.commit(self)
|
||||||
self.gui.start_content_server(check_started=False)
|
self.gui.start_content_server(check_started=False)
|
||||||
while not self.gui.content_server.is_running and self.gui.content_server.exception is None:
|
while not self.gui.content_server.is_running and self.gui.content_server.exception is None:
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
@ -16,7 +16,7 @@ Here, we will show you how to integrate the |app| content server into another se
|
|||||||
Using a reverse proxy
|
Using a reverse proxy
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
This is the simplest approach as it allows you to use the binary calibre install with no external dependencies/system integration requirements.
|
A reverse proxy is when your normal server accepts incoming requests and passes them onto the calibre server. It then reads the response from the calibre server and forwards it to the client. This means that you can simply run the calibre server as normal without trying to integrate it closely with your main server, and you can take advantage of whatever authentication systems you main server has in place. This is the simplest approach as it allows you to use the binary calibre install with no external dependencies/system integration requirements. Below, is an example of how to achieve this with Apache as your main server, but it will work with any server that supports Reverse Proxies.
|
||||||
|
|
||||||
First start the |app| content server as shown below::
|
First start the |app| content server as shown below::
|
||||||
|
|
||||||
@ -33,7 +33,7 @@ The exact technique for enabling the proxy modules will vary depending on your A
|
|||||||
RewriteRule ^/calibre/(.*) http://localhost:8080/calibre/$1 [proxy]
|
RewriteRule ^/calibre/(.*) http://localhost:8080/calibre/$1 [proxy]
|
||||||
RewriteRule ^/calibre http://localhost:8080 [proxy]
|
RewriteRule ^/calibre http://localhost:8080 [proxy]
|
||||||
|
|
||||||
That's all, you will now be able to access the |app| Content Server under the /calibre URL in your apache server.
|
That's all, you will now be able to access the |app| Content Server under the /calibre URL in your apache server. The above rules pass all requests under /calibre to the calibre server running on port 8080 and thanks to the --url-prefix option above, the calibre server handles them transparently.
|
||||||
|
|
||||||
.. note:: If you are willing to devote an entire VirtualHost to the content server, then there is no need to use --url-prefix and RewriteRule, instead just use the ProxyPass directive.
|
.. note:: If you are willing to devote an entire VirtualHost to the content server, then there is no need to use --url-prefix and RewriteRule, instead just use the ProxyPass directive.
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ import sys, traceback, cStringIO
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
from threading import RLock
|
from threading import RLock
|
||||||
|
|
||||||
|
from calibre import isbytestring, force_unicode, as_unicode
|
||||||
|
|
||||||
class Stream(object):
|
class Stream(object):
|
||||||
|
|
||||||
@ -63,15 +63,16 @@ class FileStream(Stream):
|
|||||||
|
|
||||||
class HTMLStream(Stream):
|
class HTMLStream(Stream):
|
||||||
|
|
||||||
|
color = {
|
||||||
|
DEBUG: '<span style="color:green">',
|
||||||
|
INFO:'<span>',
|
||||||
|
WARN: '<span style="color:yellow">',
|
||||||
|
ERROR: '<span style="color:red">'
|
||||||
|
}
|
||||||
|
normal = '</span>'
|
||||||
|
|
||||||
def __init__(self, stream=sys.stdout):
|
def __init__(self, stream=sys.stdout):
|
||||||
Stream.__init__(self, stream)
|
Stream.__init__(self, stream)
|
||||||
self.color = {
|
|
||||||
DEBUG: '<span style="color:green">',
|
|
||||||
INFO:'<span>',
|
|
||||||
WARN: '<span style="color:yellow">',
|
|
||||||
ERROR: '<span style="color:red">'
|
|
||||||
}
|
|
||||||
self.normal = '</span>'
|
|
||||||
|
|
||||||
def prints(self, level, *args, **kwargs):
|
def prints(self, level, *args, **kwargs):
|
||||||
self.stream.write(self.color[level])
|
self.stream.write(self.color[level])
|
||||||
@ -82,6 +83,46 @@ class HTMLStream(Stream):
|
|||||||
def flush(self):
|
def flush(self):
|
||||||
self.stream.flush()
|
self.stream.flush()
|
||||||
|
|
||||||
|
class UnicodeHTMLStream(HTMLStream):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.clear()
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def prints(self, level, *args, **kwargs):
|
||||||
|
col = self.color[level]
|
||||||
|
if col != self.last_col:
|
||||||
|
if self.data:
|
||||||
|
self.data.append(self.normal)
|
||||||
|
self.data.append(col)
|
||||||
|
self.last_col = col
|
||||||
|
|
||||||
|
sep = kwargs.get(u'sep', u' ')
|
||||||
|
end = kwargs.get(u'end', u'\n')
|
||||||
|
|
||||||
|
for arg in args:
|
||||||
|
if isbytestring(arg):
|
||||||
|
arg = force_unicode(arg)
|
||||||
|
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
|
||||||
|
def html(self):
|
||||||
|
end = self.normal if self.data else u''
|
||||||
|
return u''.join(self.data) + end
|
||||||
|
|
||||||
|
|
||||||
class Log(object):
|
class Log(object):
|
||||||
|
|
||||||
DEBUG = DEBUG
|
DEBUG = DEBUG
|
||||||
@ -124,4 +165,25 @@ class ThreadSafeLog(Log):
|
|||||||
with self._lock:
|
with self._lock:
|
||||||
Log.prints(self, *args, **kwargs)
|
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()
|
default_log = Log()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user