Sync to trunk.

This commit is contained in:
John Schember 2009-09-12 19:40:03 -04:00
commit de809a8fdf
83 changed files with 13464 additions and 18632 deletions

BIN
resources/images/star.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -186,8 +186,13 @@ class Build(Command):
parser.add_option('-1', '--only', choices=choices, default='all', parser.add_option('-1', '--only', choices=choices, default='all',
help=('Build only the named extension. Available: '+ help=('Build only the named extension. Available: '+
', '.join(choices)+'. Default:%default')) ', '.join(choices)+'. Default:%default'))
parser.add_option('--no-compile', default=False, action='store_true',
help='Skip compiling all C/C++ extensions.')
def run(self, opts): def run(self, opts):
if opts.no_compile:
self.info('--no-compile specified, skipping compilation')
return
self.obj_dir = os.path.join(os.path.dirname(SRC), 'build', 'objects') self.obj_dir = os.path.join(os.path.dirname(SRC), 'build', 'objects')
if not os.path.exists(self.obj_dir): if not os.path.exists(self.obj_dir):
os.makedirs(self.obj_dir) os.makedirs(self.obj_dir)

View File

@ -43,7 +43,7 @@ class Develop(Command):
sub_commands = ['build', 'resources', 'gui'] sub_commands = ['build', 'resources', 'gui']
def add_options(self, parser): def add_options(self, parser):
parser.add_option('--prefix', parser.add_option('--prefix', '--root',
help='Binaries will be installed in <prefix>/bin') help='Binaries will be installed in <prefix>/bin')
def pre_sub_commands(self, opts): def pre_sub_commands(self, opts):
@ -91,7 +91,9 @@ class Develop(Command):
pass pass
def run_postinstall(self): def run_postinstall(self):
subprocess.check_call(['calibre_postinstall']) env = dict(**os.environ)
env['DESTDIR'] = self.prefix
subprocess.check_call(['calibre_postinstall', '--use-destdir'], env=env)
def success(self): def success(self):
self.info('\nDevelopment environment successfully setup') self.info('\nDevelopment environment successfully setup')
@ -119,6 +121,8 @@ class Develop(Command):
path=self.path, resources=self.resources, path=self.path, resources=self.resources,
extensions=self.extensions) extensions=self.extensions)
path = self.j(self.bindir, name) path = self.j(self.bindir, name)
if not os.path.exists(self.bindir):
os.makedirs(self.bindir)
self.info('Installing binary:', path) self.info('Installing binary:', path)
open(path, 'wb').write(script) open(path, 'wb').write(script)
os.chmod(path, self.MODE) os.chmod(path, self.MODE)
@ -134,10 +138,10 @@ class Install(Develop):
The default <prefix> is the prefix of your python installation. The default <prefix> is the prefix of your python installation.
''') ''')
sub_commands = ['build'] sub_commands = ['build', 'gui']
def add_options(self, parser): def add_options(self, parser):
parser.add_option('--prefix', help='Installation prefix') parser.add_option('--prefix', '--root', help='Installation prefix')
parser.add_option('--libdir', help='Where to put calibre library files') parser.add_option('--libdir', help='Where to put calibre library files')
parser.add_option('--bindir', help='Where to install calibre binaries') parser.add_option('--bindir', help='Where to install calibre binaries')
parser.add_option('--sharedir', help='Where to install calibre data files') parser.add_option('--sharedir', help='Where to install calibre data files')
@ -194,6 +198,14 @@ class Sdist(Command):
shutil.copytree(p, self.j(tdir, p)) shutil.copytree(p, self.j(tdir, p))
else: else:
shutil.copy2(p, d) shutil.copy2(p, d)
for x in os.walk(os.path.join(self.SRC, 'calibre')):
for f in x[-1]:
if not f.endswith('_ui.py'): continue
f = os.path.join(x[0], f)
f = os.path.relpath(f)
dest = os.path.join(tdir, self.d(f))
shutil.copy2(f, dest)
self.info('\tCreating tarfile...') self.info('\tCreating tarfile...')
subprocess.check_call(' '.join(['tar', '-czf', self.a(self.DEST), '*']), subprocess.check_call(' '.join(['tar', '-czf', self.a(self.DEST), '*']),
cwd=tdir, shell=True) cwd=tdir, shell=True)

View File

@ -20,7 +20,7 @@ class VMInstaller(Command):
VM_NAME = None VM_NAME = None
FREEZE_COMMAND = None FREEZE_COMMAND = None
FREEZE_TEMPLATE = 'python setup.py {freeze_command}' FREEZE_TEMPLATE = 'python setup.py {freeze_command}'
SHUTDOWN_CMD = ['sudo', 'shutdown', '-h', 'now'] SHUTDOWN_CMD = ['sudo', 'poweroff']
IS_64_BIT = False IS_64_BIT = False
BUILD_CMD = 'ssh -t %s bash build-calibre' BUILD_CMD = 'ssh -t %s bash build-calibre'

View File

@ -46,13 +46,12 @@ class LinuxFreeze(Command):
'/lib/libz.so.1', '/lib/libz.so.1',
'/usr/lib/libtiff.so.3', '/usr/lib/libtiff.so.3',
'/lib/libbz2.so.1', '/lib/libbz2.so.1',
'/usr/lib/libpoppler.so.4', '/usr/lib/libpoppler.so.5',
'/usr/lib/libpoppler-qt4.so.3', '/usr/lib/libpoppler-qt4.so.3',
'/usr/lib/libxml2.so.2', '/usr/lib/libxml2.so.2',
'/usr/lib/libopenjpeg.so.2', '/usr/lib/libopenjpeg.so.2',
'/usr/lib/libxslt.so.1', '/usr/lib/libxslt.so.1',
'/usr/lib64/libjpeg.so.7'.replace('64', '64' if is64bit '/usr/lib/libjpeg.so.7',
else ''),
'/usr/lib/libxslt.so.1', '/usr/lib/libxslt.so.1',
'/usr/lib/libgthread-2.0.so.0', '/usr/lib/libgthread-2.0.so.0',
'/usr/lib/gcc/***-pc-linux-gnu/4.4.1/libstdc++.so.6'.replace('***', '/usr/lib/gcc/***-pc-linux-gnu/4.4.1/libstdc++.so.6'.replace('***',
@ -60,7 +59,7 @@ class LinuxFreeze(Command):
'/usr/lib/libpng12.so.0', '/usr/lib/libpng12.so.0',
'/usr/lib/libexslt.so.0', '/usr/lib/libexslt.so.0',
'/usr/lib/libMagickWand.so', '/usr/lib/libMagickWand.so',
'/usr/lib/libMagickCore.so', '/usr/lib/libMagickCore.so.2',
'/usr/lib/libgcrypt.so.11', '/usr/lib/libgcrypt.so.11',
'/usr/lib/libgpg-error.so.0', '/usr/lib/libgpg-error.so.0',
'/usr/lib/libphonon.so.4', '/usr/lib/libphonon.so.4',

View File

@ -25,3 +25,4 @@ class OSX32(VMInstaller):
VM = '/vmware/bin/%s'%VM_NAME VM = '/vmware/bin/%s'%VM_NAME
FREEZE_COMMAND = 'osx32_freeze' FREEZE_COMMAND = 'osx32_freeze'
BUILD_PREFIX = VMInstaller.BUILD_PREFIX + ['source ~/.profile'] BUILD_PREFIX = VMInstaller.BUILD_PREFIX + ['source ~/.profile']
SHUTDOWN_CMD = ['sudo', 'halt']

View File

@ -26,6 +26,7 @@ class Win32(VMInstaller):
VM_NAME = 'xp_build' VM_NAME = 'xp_build'
VM = '/vmware/bin/%s'%VM_NAME VM = '/vmware/bin/%s'%VM_NAME
FREEZE_COMMAND = 'win32_freeze' FREEZE_COMMAND = 'win32_freeze'
SHUTDOWN_CMD = ['shutdown', '-s']
def download_installer(self): def download_installer(self):
installer = self.installer() installer = self.installer()

View File

@ -6,7 +6,7 @@ __license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import sys, os, shutil, subprocess, re, time import sys, os, shutil, subprocess, re, time, glob
from datetime import datetime from datetime import datetime
from setup import Command, __appname__, __version__ from setup import Command, __appname__, __version__
@ -31,6 +31,11 @@ class Stage2(Command):
description = 'Stage 2 of the publish process' description = 'Stage 2 of the publish process'
sub_commands = ['linux', 'win', 'osx'] sub_commands = ['linux', 'win', 'osx']
def pre_sub_commands(self, opts):
for x in glob.glob(os.path.join(self.d(self.SRC), 'dist', '*')):
os.remove(x)
class Stage3(Command): class Stage3(Command):
description = 'Stage 3 of the publish process' description = 'Stage 3 of the publish process'
@ -88,7 +93,7 @@ if os.environ.get('CALIBRE_BUILDBOT', None) == '1':
else: else:
class UploadRss(Command): class UploadRss(Command):
description = 'Generate and uplaod a RSS feed of calibre releases' description = 'Generate and upload a RSS feed of calibre releases'
from bzrlib import log as blog from bzrlib import log as blog
@ -150,7 +155,9 @@ else:
bzr_path = os.path.expanduser('~/work/calibre') bzr_path = os.path.expanduser('~/work/calibre')
b = branch.Branch.open(bzr_path) b = branch.Branch.open(bzr_path)
lf = UploadRss.ChangelogFormatter() lf = UploadRss.ChangelogFormatter()
self.info('\tGenerating bzr log...')
log.show_log(b, lf) log.show_log(b, lf)
lf.rss.write_xml(open('/tmp/releases.xml', 'wb')) lf.rss.write_xml(open('/tmp/releases.xml', 'wb'))
#subprocess.check_call('scp /tmp/releases.xml divok:/var/www/calibre.kovidgoyal.net/htdocs/downloads'.split()) self.info('\tUploading RSS to server...')
subprocess.check_call('scp /tmp/releases.xml divok:/var/www/calibre.kovidgoyal.net/htdocs/downloads'.split())

View File

@ -221,9 +221,13 @@ def get_proxies():
proxies['ftp'] = server proxies['ftp'] = server
settings.Close() settings.Close()
except Exception, e: except Exception, e:
print('Unable to detect proxy settings: %s' % str(e)) prints('Unable to detect proxy settings: %s' % str(e))
for x in list(proxies):
if len(proxies[x]) < 5:
prints('Removing invalid', x, 'proxy:', proxies[x])
del proxies[x]
if proxies: if proxies:
print('Using proxies: %s' % proxies) prints('Using proxies: %s' % proxies)
return proxies return proxies

View File

@ -2,7 +2,7 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
__appname__ = 'calibre' __appname__ = 'calibre'
__version__ = '0.6.11' __version__ = '0.6.12'
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>" __author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
import re import re

View File

@ -367,6 +367,7 @@ from calibre.devices.prs500.driver import PRS500
from calibre.devices.prs505.driver import PRS505 from calibre.devices.prs505.driver import PRS505
from calibre.devices.prs700.driver import PRS700 from calibre.devices.prs700.driver import PRS700
from calibre.devices.android.driver import ANDROID from calibre.devices.android.driver import ANDROID
from calibre.devices.eslick.driver import ESLICK
plugins = [HTML2ZIP] plugins = [HTML2ZIP]
plugins += [ plugins += [
@ -416,7 +417,8 @@ plugins += [
PRS700, PRS700,
ANDROID, ANDROID,
CYBOOK_OPUS, CYBOOK_OPUS,
COOL_ER COOL_ER,
ESLICK
] ]
plugins += [x for x in list(locals().values()) if isinstance(x, type) and \ plugins += [x for x in list(locals().values()) if isinstance(x, type) and \
x.__name__.endswith('MetadataReader')] x.__name__.endswith('MetadataReader')]

View File

@ -155,6 +155,9 @@ def debug_device_driver():
print print
print "Don't forget to send the file /tmp/ioreg.txt as well" print "Don't forget to send the file /tmp/ioreg.txt as well"
if iswindows:
raw_input('Press Enter to continue...')
def add_simple_plugin(path_to_plugin): def add_simple_plugin(path_to_plugin):
import tempfile, zipfile, shutil import tempfile, zipfile, shutil

View File

@ -0,0 +1,10 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

View File

@ -0,0 +1,39 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from calibre.devices.usbms.driver import USBMS
class ESLICK(USBMS):
name = 'ESlick Device Interface'
gui_name = 'Foxit ESlick'
description = _('Communicate with the ESlick eBook reader.')
author = _('Kovid Goyal')
supported_platforms = ['windows', 'osx', 'linux']
# Ordered list of supported formats
FORMATS = ['pdf', 'txt']
VENDOR_ID = [0x04cc]
PRODUCT_ID = [0x1a64]
BCD = [0x0110]
VENDOR_NAME = 'FOXIT'
WINDOWS_MAIN_MEM = 'ESLICK_USB_DEVICE'
WINDOWS_CARD_A_MEM = 'ESLICK_USB_DEVICE'
#OSX_MAIN_MEM = 'Kindle Internal Storage Media'
#OSX_CARD_A_MEM = 'Kindle Card Storage Media'
MAIN_MEMORY_VOLUME_LABEL = 'ESlick Main Memory'
STORAGE_CARD_VOLUME_LABEL = 'ESlick Storage Card'
SUPPORTS_SUB_DIRS = True

View File

@ -238,7 +238,7 @@ class HTMLPreProcessor(object):
) )
end_rules = [] end_rules = []
if getattr(self.extra_opts, 'unwrap_factor', None): if getattr(self.extra_opts, 'unwrap_factor', 0.0) > 0.01:
length = line_length(html, getattr(self.extra_opts, 'unwrap_factor')) length = line_length(html, getattr(self.extra_opts, 'unwrap_factor'))
if length: if length:
end_rules.append( end_rules.append(

View File

@ -262,9 +262,10 @@ class HTMLInput(InputFormatPlugin):
) )
), ),
OptionRecommendation(name='unwrap_factor', recommended_value=0.5, OptionRecommendation(name='unwrap_factor', recommended_value=0.0,
help=_('Average line length for line breaking if the HTML is from a ' help=_('Average line length for line breaking if the HTML is from a '
'previous partial conversion of a PDF file.')), 'previous partial conversion of a PDF file. Default is %default '
'which disables this.')),
]) ])

View File

@ -153,7 +153,7 @@ class ResultList(list):
d = date(entry) d = date(entry)
if d: if d:
default = datetime.utcnow() default = datetime.utcnow()
default = datetime(default.year, default.month, 1) default = datetime(default.year, default.month, 15)
d = parser.parse(d[0].text, default=default) d = parser.parse(d[0].text, default=default)
else: else:
d = None d = None

View File

@ -309,7 +309,8 @@ class MobiReader(object):
try: try:
root = html.fromstring(self.processed_html) root = html.fromstring(self.processed_html)
if len(root.xpath('//html')) > 5: if len(root.xpath('//html')) > 5:
root = html.fromstring(self.processed_html.replace('\x0c', '')) root = html.fromstring(self.processed_html.replace('\x0c',
'').replace('\x14', ''))
except: except:
self.log.warning('MOBI markup appears to contain random bytes. Stripping.') self.log.warning('MOBI markup appears to contain random bytes. Stripping.')
self.processed_html = self.remove_random_bytes(self.processed_html) self.processed_html = self.remove_random_bytes(self.processed_html)

View File

@ -773,6 +773,7 @@ class Manifest(object):
data = self.oeb.decode(data) data = self.oeb.decode(data)
data = self.oeb.html_preprocessor(data) data = self.oeb.html_preprocessor(data)
# Remove DOCTYPE declaration as it messes up parsing # Remove DOCTYPE declaration as it messes up parsing
# Inparticular it causes tostring to insert xmlns # Inparticular it causes tostring to insert xmlns
# declarations, which messes up the coercing logic # declarations, which messes up the coercing logic

View File

@ -8,6 +8,7 @@ __docformat__ = 'restructuredtext en'
import textwrap import textwrap
from xml.sax.saxutils import escape from xml.sax.saxutils import escape
from itertools import repeat
from lxml import etree from lxml import etree
@ -31,6 +32,7 @@ class Jacket(object):
<h1 class="calibre_rescale_180">%(title)s</h1> <h1 class="calibre_rescale_180">%(title)s</h1>
<h2 class="calibre_rescale_140">%(jacket)s</h2> <h2 class="calibre_rescale_140">%(jacket)s</h2>
<div class="calibre_rescale_100">%(series)s</div> <div class="calibre_rescale_100">%(series)s</div>
<div class="calibre_rescale_100">%(rating)s</div>
<div class="calibre_rescale_100">%(tags)s</div> <div class="calibre_rescale_100">%(tags)s</div>
</div> </div>
<div style="margin-top:2em" class="calibre_rescale_100"> <div style="margin-top:2em" class="calibre_rescale_100">
@ -54,6 +56,23 @@ class Jacket(object):
img.getparent().remove(img) img.getparent().remove(img)
return return
def get_rating(self, rating):
ans = ''
if rating is None:
return
try:
num = float(rating)/2
except:
return ans
num = max(0, num)
num = min(num, 5)
if num < 1:
return ans
id, href = self.oeb.manifest.generate('star', 'star.png')
self.oeb.manifest.add(id, href, 'image/png', data=I('star.png', data=True))
ans = 'Rating: ' + ''.join(repeat('<img style="vertical-align:text-top" alt="star" src="%s" />'%href, num))
return ans
def insert_metadata(self, mi): def insert_metadata(self, mi):
self.log('Inserting metadata into book...') self.log('Inserting metadata into book...')
comments = mi.comments comments = mi.comments
@ -87,7 +106,7 @@ class Jacket(object):
html = self.JACKET_TEMPLATE%dict(xmlns=XPNSMAP['h'], html = self.JACKET_TEMPLATE%dict(xmlns=XPNSMAP['h'],
title=escape(title), comments=escape(comments), title=escape(title), comments=escape(comments),
jacket=escape(_('Book Jacket')), series=series, jacket=escape(_('Book Jacket')), series=series,
tags=tags) tags=tags, rating=self.get_rating(mi.rating))
id, href = self.oeb.manifest.generate('jacket', 'jacket.xhtml') id, href = self.oeb.manifest.generate('jacket', 'jacket.xhtml')
root = etree.fromstring(html) root = etree.fromstring(html)
item = self.oeb.manifest.add(id, href, guess_type(href)[0], data=root) item = self.oeb.manifest.add(id, href, guess_type(href)[0], data=root)

View File

@ -25,7 +25,7 @@ class PDBInput(InputFormatPlugin):
OptionRecommendation(name='print_formatted_paras', recommended_value=False, OptionRecommendation(name='print_formatted_paras', recommended_value=False,
help=_('Normally calibre treats blank lines as paragraph markers. ' help=_('Normally calibre treats blank lines as paragraph markers. '
'With this option it will assume that every line starting with ' 'With this option it will assume that every line starting with '
'an indent (either a tab or 2+ spaces) represents a paragraph.' 'an indent (either a tab or 2+ spaces) represents a paragraph. '
'Paragraphs end when the next line that starts with an indent ' 'Paragraphs end when the next line that starts with an indent '
'is reached.')), 'is reached.')),
]) ])

View File

@ -25,7 +25,7 @@ class TXTInput(InputFormatPlugin):
OptionRecommendation(name='print_formatted_paras', recommended_value=False, OptionRecommendation(name='print_formatted_paras', recommended_value=False,
help=_('Normally calibre treats blank lines as paragraph markers. ' help=_('Normally calibre treats blank lines as paragraph markers. '
'With this option it will assume that every line starting with ' 'With this option it will assume that every line starting with '
'an indent (either a tab or 2+ spaces) represents a paragraph.' 'an indent (either a tab or 2+ spaces) represents a paragraph. '
'Paragraphs end when the next line that starts with an indent ' 'Paragraphs end when the next line that starts with an indent '
'is reached.')), 'is reached.')),
OptionRecommendation(name='markdown', recommended_value=False, OptionRecommendation(name='markdown', recommended_value=False,

View File

@ -3,7 +3,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
""" The GUI """ """ The GUI """
import os import os
from PyQt4.QtCore import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, QSize, \ from PyQt4.QtCore import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, QSize, \
QByteArray, QUrl, QTranslator, QCoreApplication, QThread QByteArray, QTranslator, QCoreApplication, QThread
from PyQt4.QtGui import QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \ from PyQt4.QtGui import QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \
QIcon, QTableView, QApplication, QDialog, QPushButton QIcon, QTableView, QApplication, QDialog, QPushButton
@ -23,8 +23,6 @@ ALL_COLUMNS = ['title', 'authors', 'size', 'timestamp', 'rating', 'publisher',
def _config(): def _config():
c = Config('gui', 'preferences for the calibre GUI') c = Config('gui', 'preferences for the calibre GUI')
c.add_opt('frequently_used_directories', default=[],
help=_('Frequently used directories'))
c.add_opt('send_to_storage_card_by_default', default=False, c.add_opt('send_to_storage_card_by_default', default=False,
help=_('Send file to storage card instead of main memory by default')) help=_('Send file to storage card instead of main memory by default'))
c.add_opt('confirm_delete', default=False, c.add_opt('confirm_delete', default=False,
@ -83,6 +81,8 @@ def _config():
help='Search history for the LRF viewer') help='Search history for the LRF viewer')
c.add_opt('scheduler_search_history', default=[], c.add_opt('scheduler_search_history', default=[],
help='Search history for the recipe scheduler') help='Search history for the recipe scheduler')
c.add_opt('worker_limit', default=6,
help=_('Maximum number of waiting worker processes'))
return ConfigProxy(c) return ConfigProxy(c)
@ -377,15 +377,8 @@ def file_icon_provider():
global _file_icon_provider global _file_icon_provider
return _file_icon_provider return _file_icon_provider
_sidebar_directories = []
def set_sidebar_directories(dirs):
global _sidebar_directories
if dirs is None:
dirs = config['frequently_used_directories']
_sidebar_directories = [QUrl.fromLocalFile(i) for i in dirs]
class FileDialog(QObject): class FileDialog(QObject):
def __init__(self, title='Choose Files', def __init__(self, title=_('Choose Files'),
filters=[], filters=[],
add_all_files_filter=True, add_all_files_filter=True,
parent=None, parent=None,
@ -394,7 +387,6 @@ class FileDialog(QObject):
mode = QFileDialog.ExistingFiles, mode = QFileDialog.ExistingFiles,
): ):
QObject.__init__(self) QObject.__init__(self)
initialize_file_icon_provider()
ftext = '' ftext = ''
if filters: if filters:
for filter in filters: for filter in filters:
@ -410,63 +402,42 @@ class FileDialog(QObject):
self.selected_files = None self.selected_files = None
self.fd = None self.fd = None
if islinux: initial_dir = dynamic.get(self.dialog_name, os.path.expanduser('~'))
self.fd = QFileDialog(parent) if not isinstance(initial_dir, basestring):
self.fd.setFileMode(mode) initial_dir = os.path.expanduser('~')
self.fd.setIconProvider(_file_icon_provider) self.selected_files = []
self.fd.setModal(modal) if mode == QFileDialog.AnyFile:
self.fd.setNameFilter(ftext) f = unicode(QFileDialog.getSaveFileName(parent, title, initial_dir, ftext, ""))
self.fd.setWindowTitle(title) if f and os.path.exists(f):
state = dynamic[self.dialog_name] self.selected_files.append(f)
if not state or not self.fd.restoreState(state): elif mode == QFileDialog.ExistingFile:
self.fd.setDirectory(os.path.expanduser('~')) f = unicode(QFileDialog.getOpenFileName(parent, title, initial_dir, ftext, ""))
osu = [i for i in self.fd.sidebarUrls()] if f and os.path.exists(f):
self.fd.setSidebarUrls(osu + _sidebar_directories) self.selected_files.append(f)
QObject.connect(self.fd, SIGNAL('accepted()'), self.save_dir) elif mode == QFileDialog.ExistingFiles:
self.accepted = self.fd.exec_() == QFileDialog.Accepted fs = QFileDialog.getOpenFileNames(parent, title, initial_dir, ftext, "")
for f in fs:
f = unicode(f)
if f and os.path.exists(f):
self.selected_files.append(f)
else: else:
dir = dynamic.get(self.dialog_name, os.path.expanduser('~')) opts = QFileDialog.ShowDirsOnly if mode == QFileDialog.DirectoryOnly else QFileDialog.Option()
self.selected_files = [] f = unicode(QFileDialog.getExistingDirectory(parent, title, initial_dir, opts))
if mode == QFileDialog.AnyFile: if os.path.exists(f):
f = qstring_to_unicode( self.selected_files.append(f)
QFileDialog.getSaveFileName(parent, title, dir, ftext, "")) if self.selected_files:
if os.path.exists(f): self.selected_files = [unicode(q) for q in self.selected_files]
self.selected_files.append(f) saved_loc = self.selected_files[0]
elif mode == QFileDialog.ExistingFile: if os.path.isfile(saved_loc):
f = qstring_to_unicode( saved_loc = os.path.dirname(saved_loc)
QFileDialog.getOpenFileName(parent, title, dir, ftext, "")) dynamic[self.dialog_name] = saved_loc
if os.path.exists(f): self.accepted = bool(self.selected_files)
self.selected_files.append(f)
elif mode == QFileDialog.ExistingFiles:
fs = QFileDialog.getOpenFileNames(parent, title, dir, ftext, "")
for f in fs:
if os.path.exists(qstring_to_unicode(f)):
self.selected_files.append(f)
else:
opts = QFileDialog.ShowDirsOnly if mode == QFileDialog.DirectoryOnly else QFileDialog.Option()
f = qstring_to_unicode(
QFileDialog.getExistingDirectory(parent, title, dir, opts))
if os.path.exists(f):
self.selected_files.append(f)
if self.selected_files:
self.selected_files = [unicode(q) for q in self.selected_files]
saved_loc = self.selected_files[0]
if os.path.isfile(saved_loc):
saved_loc = os.path.dirname(saved_loc)
dynamic[self.dialog_name] = saved_loc
self.accepted = bool(self.selected_files)
def get_files(self): def get_files(self):
if islinux and self.fd.result() != self.fd.Accepted:
return tuple()
if self.selected_files is None: if self.selected_files is None:
return tuple(os.path.abspath(qstring_to_unicode(i)) for i in self.fd.selectedFiles()) return tuple(os.path.abspath(qstring_to_unicode(i)) for i in self.fd.selectedFiles())
return tuple(self.selected_files) return tuple(self.selected_files)
def save_dir(self):
if self.fd:
dynamic[self.dialog_name] = self.fd.saveState()
def choose_dir(window, name, title): def choose_dir(window, name, title):
fd = FileDialog(title, [], False, window, name=name, fd = FileDialog(title, [], False, window, name=name,

View File

@ -18,6 +18,24 @@ from calibre.ebooks.metadata.opf2 import OPFCreator
from calibre.ptempfile import PersistentTemporaryFile from calibre.ptempfile import PersistentTemporaryFile
from calibre.gui2.convert import Widget from calibre.gui2.convert import Widget
def create_opf_file(db, book_id):
mi = db.get_metadata(book_id, index_is_id=True)
mi.application_id = uuid.uuid4()
opf = OPFCreator(os.getcwdu(), mi)
opf_file = PersistentTemporaryFile('.opf')
opf.render(opf_file)
opf_file.close()
return mi, opf_file
def create_cover_file(db, book_id):
cover = db.cover(book_id, index_is_id=True)
cf = None
if cover:
cf = PersistentTemporaryFile('.jpeg')
cf.write(cover)
cf.close()
return cf
class MetadataWidget(Widget, Ui_Form): class MetadataWidget(Widget, Ui_Form):
TITLE = _('Metadata') TITLE = _('Metadata')
@ -181,12 +199,7 @@ class MetadataWidget(Widget, Ui_Form):
self.cover_file = self.opf_file = None self.cover_file = self.opf_file = None
if self.db is not None: if self.db is not None:
self.db.set_metadata(self.book_id, self.user_mi) self.db.set_metadata(self.book_id, self.user_mi)
self.mi = self.db.get_metadata(self.book_id, index_is_id=True) self.mi, self.opf_file = create_opf_file(self.db, self.book_id)
self.mi.application_id = uuid.uuid4()
opf = OPFCreator(os.getcwdu(), self.mi)
self.opf_file = PersistentTemporaryFile('.opf')
opf.render(self.opf_file)
self.opf_file.close()
if self.cover_changed and self.cover_data is not None: if self.cover_changed and self.cover_data is not None:
self.db.set_cover(self.book_id, self.cover_data) self.db.set_cover(self.book_id, self.cover_data)
cover = self.db.cover(self.book_id, index_is_id=True) cover = self.db.cover(self.book_id, index_is_id=True)

View File

@ -5,7 +5,7 @@ __copyright__ = '2009, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
from calibre.gui2.convert.pdf_input_ui import Ui_Form from calibre.gui2.convert.pdf_input_ui import Ui_Form
from calibre.gui2.convert import Widget from calibre.gui2.convert import Widget, QDoubleSpinBox
class PluginWidget(Widget, Ui_Form): class PluginWidget(Widget, Ui_Form):
@ -17,3 +17,8 @@ class PluginWidget(Widget, Ui_Form):
['no_images', 'unwrap_factor']) ['no_images', 'unwrap_factor'])
self.db, self.book_id = db, book_id self.db, self.book_id = db, book_id
self.initialize_options(get_option, get_help, db, book_id) self.initialize_options(get_option, get_help, db, book_id)
def set_value_handler(self, g, val):
if val is None and isinstance(g, QDoubleSpinBox):
g.setValue(0.0)
return True

View File

@ -68,6 +68,36 @@ class GroupModel(QAbstractListModel):
return QVariant(f) return QVariant(f)
return NONE return NONE
def get_preferred_input_format_for_book(db, book_id):
recs = load_specifics(db, book_id)
if recs:
return recs.get('gui_preferred_input_format', None)
def get_available_formats_for_book(db, book_id):
available_formats = db.formats(book_id, index_is_id=True)
if not available_formats:
available_formats = ''
return set([x.lower() for x in
available_formats.split(',')])
def get_supported_input_formats_for_book(db, book_id):
available_formats = get_available_formats_for_book(db, book_id)
input_formats = set([x.lower() for x in supported_input_formats()])
input_formats = sorted(available_formats.intersection(input_formats))
if not input_formats:
raise NoSupportedInputFormats
return input_formats
def get_input_format_for_book(db, book_id, pref):
if pref is None:
pref = get_preferred_input_format_for_book(db, book_id)
input_formats = get_supported_input_formats_for_book(db, book_id)
input_format = pref if pref in input_formats else \
sort_formats_by_preference(input_formats, prefs['input_format_order'])[0]
return input_format, input_formats
class Config(ResizableDialog, Ui_Dialog): class Config(ResizableDialog, Ui_Dialog):
''' '''
Configuration dialog for single book conversion. If accepted, has the Configuration dialog for single book conversion. If accepted, has the
@ -86,12 +116,6 @@ class Config(ResizableDialog, Ui_Dialog):
preferred_input_format=None, preferred_output_format=None): preferred_input_format=None, preferred_output_format=None):
ResizableDialog.__init__(self, parent) ResizableDialog.__init__(self, parent)
if preferred_input_format is None and db is not None:
recs = load_specifics(db, book_id)
if recs:
preferred_input_format = recs.get('gui_preferred_input_format',
None)
self.setup_input_output_formats(db, book_id, preferred_input_format, self.setup_input_output_formats(db, book_id, preferred_input_format,
preferred_output_format) preferred_output_format)
self.db, self.book_id = db, book_id self.db, self.book_id = db, book_id
@ -194,22 +218,10 @@ class Config(ResizableDialog, Ui_Dialog):
preferred_output_format): preferred_output_format):
if preferred_output_format: if preferred_output_format:
preferred_output_format = preferred_output_format.lower() preferred_output_format = preferred_output_format.lower()
available_formats = db.formats(book_id, index_is_id=True)
if not available_formats:
available_formats = ''
available_formats = set([x.lower() for x in
available_formats.split(',')])
input_formats = set([x.lower() for x in supported_input_formats()])
input_formats = \
sorted(available_formats.intersection(input_formats))
if not input_formats:
raise NoSupportedInputFormats
output_formats = sorted(available_output_formats()) output_formats = sorted(available_output_formats())
output_formats.remove('oeb') output_formats.remove('oeb')
preferred_input_format = preferred_input_format if \ input_format, input_formats = get_input_format_for_book(db, book_id,
preferred_input_format in input_formats else \ preferred_input_format)
sort_formats_by_preference(input_formats,
prefs['input_format_order'])[0]
preferred_output_format = preferred_output_format if \ preferred_output_format = preferred_output_format if \
preferred_output_format in output_formats else \ preferred_output_format in output_formats else \
sort_formats_by_preference(output_formats, sort_formats_by_preference(output_formats,
@ -218,7 +230,7 @@ class Config(ResizableDialog, Ui_Dialog):
input_formats]))) input_formats])))
self.output_formats.addItems(list(map(QString, [x.upper() for x in self.output_formats.addItems(list(map(QString, [x.upper() for x in
output_formats]))) output_formats])))
self.input_formats.setCurrentIndex(input_formats.index(preferred_input_format)) self.input_formats.setCurrentIndex(input_formats.index(input_format))
self.output_formats.setCurrentIndex(output_formats.index(preferred_output_format)) self.output_formats.setCurrentIndex(output_formats.index(preferred_output_format))
def show_pane(self, index): def show_pane(self, index):

View File

@ -10,7 +10,7 @@ from PyQt4.Qt import QDialog, QListWidgetItem, QIcon, \
QDialogButtonBox, QTabWidget, QBrush, QLineEdit, \ QDialogButtonBox, QTabWidget, QBrush, QLineEdit, \
QProgressDialog QProgressDialog
from calibre.constants import islinux, iswindows, isosx from calibre.constants import iswindows, isosx
from calibre.gui2.dialogs.config.config_ui import Ui_Dialog from calibre.gui2.dialogs.config.config_ui import Ui_Dialog
from calibre.gui2 import qstring_to_unicode, choose_dir, error_dialog, config, \ from calibre.gui2 import qstring_to_unicode, choose_dir, error_dialog, config, \
ALL_COLUMNS, NONE, info_dialog, choose_files, \ ALL_COLUMNS, NONE, info_dialog, choose_files, \
@ -354,16 +354,10 @@ class ConfigDialog(QDialog, Ui_Dialog):
self.connect(self.input_up, SIGNAL('clicked()'), self.up_input) self.connect(self.input_up, SIGNAL('clicked()'), self.up_input)
self.connect(self.input_down, SIGNAL('clicked()'), self.down_input) self.connect(self.input_down, SIGNAL('clicked()'), self.down_input)
dirs = config['frequently_used_directories']
rn = config['use_roman_numerals_for_series_number'] rn = config['use_roman_numerals_for_series_number']
self.timeout.setValue(prefs['network_timeout']) self.timeout.setValue(prefs['network_timeout'])
self.roman_numerals.setChecked(rn) self.roman_numerals.setChecked(rn)
self.new_version_notification.setChecked(config['new_version_notification']) self.new_version_notification.setChecked(config['new_version_notification'])
self.directory_list.addItems(dirs)
self.connect(self.add_button, SIGNAL('clicked(bool)'), self.add_dir)
self.connect(self.remove_button, SIGNAL('clicked(bool)'), self.remove_dir)
if not islinux:
self.dirs_box.setVisible(False)
column_map = config['column_map'] column_map = config['column_map']
for col in column_map + [i for i in ALL_COLUMNS if i not in column_map]: for col in column_map + [i for i in ALL_COLUMNS if i not in column_map]:
@ -432,6 +426,7 @@ class ConfigDialog(QDialog, Ui_Dialog):
self.port.setValue(opts.port) self.port.setValue(opts.port)
self.username.setText(opts.username) self.username.setText(opts.username)
self.password.setText(opts.password if opts.password else '') self.password.setText(opts.password if opts.password else '')
self.opt_max_opds_items.setValue(opts.max_opds_items)
self.auto_launch.setChecked(config['autolaunch_server']) self.auto_launch.setChecked(config['autolaunch_server'])
self.systray_icon.setChecked(config['systray_icon']) self.systray_icon.setChecked(config['systray_icon'])
self.sync_news.setChecked(config['upload_news_to_device']) self.sync_news.setChecked(config['upload_news_to_device'])
@ -457,6 +452,7 @@ class ConfigDialog(QDialog, Ui_Dialog):
self.connect(self.sync_news, SIGNAL('toggled(bool)'), self.connect(self.sync_news, SIGNAL('toggled(bool)'),
self.delete_news.setEnabled) self.delete_news.setEnabled)
self.setup_conversion_options() self.setup_conversion_options()
self.opt_worker_limit.setValue(config['worker_limit'])
def create_symlinks(self): def create_symlinks(self):
from calibre.utils.osx_symlinks import create_symlinks from calibre.utils.osx_symlinks import create_symlinks
@ -674,15 +670,6 @@ class ConfigDialog(QDialog, Ui_Dialog):
if dir: if dir:
self.location.setText(dir) self.location.setText(dir)
def add_dir(self):
dir = choose_dir(self, 'Add freq dir dialog', 'select directory')
if dir:
self.directory_list.addItem(dir)
def remove_dir(self):
idx = self.directory_list.currentRow()
if idx >= 0:
self.directory_list.takeItem(idx)
def accept(self): def accept(self):
mcs = unicode(self.max_cover_size.text()).strip() mcs = unicode(self.max_cover_size.text()).strip()
@ -696,6 +683,12 @@ class ConfigDialog(QDialog, Ui_Dialog):
return return
if not self.add_save.save_settings(): if not self.add_save.save_settings():
return return
wl = self.opt_worker_limit.value()
if wl%2 != 0:
wl += 1
config['worker_limit'] = wl
config['use_roman_numerals_for_series_number'] = bool(self.roman_numerals.isChecked()) config['use_roman_numerals_for_series_number'] = bool(self.roman_numerals.isChecked())
config['new_version_notification'] = bool(self.new_version_notification.isChecked()) config['new_version_notification'] = bool(self.new_version_notification.isChecked())
prefs['network_timeout'] = int(self.timeout.value()) prefs['network_timeout'] = int(self.timeout.value())
@ -722,6 +715,7 @@ class ConfigDialog(QDialog, Ui_Dialog):
sc.set('password', unicode(self.password.text()).strip()) sc.set('password', unicode(self.password.text()).strip())
sc.set('port', self.port.value()) sc.set('port', self.port.value())
sc.set('max_cover', mcs) sc.set('max_cover', mcs)
sc.set('max_opds_items', self.opt_max_opds_items.value())
config['delete_news_from_library_on_upload'] = self.delete_news.isChecked() config['delete_news_from_library_on_upload'] = self.delete_news.isChecked()
config['upload_news_to_device'] = self.sync_news.isChecked() config['upload_news_to_device'] = self.sync_news.isChecked()
config['search_as_you_type'] = self.search_as_you_type.isChecked() config['search_as_you_type'] = self.search_as_you_type.isChecked()
@ -742,10 +736,6 @@ class ConfigDialog(QDialog, Ui_Dialog):
d.exec_() d.exec_()
else: else:
self.database_location = os.path.abspath(path) self.database_location = os.path.abspath(path)
self.directories = [
qstring_to_unicode(self.directory_list.item(i).text()) for i in \
range(self.directory_list.count())]
config['frequently_used_directories'] = self.directories
QDialog.accept(self) QDialog.accept(self)
class VacThread(QThread): class VacThread(QThread):

View File

@ -281,103 +281,6 @@
</layout> </layout>
</widget> </widget>
</item> </item>
<item>
<widget class="QGroupBox" name="dirs_box">
<property name="title">
<string>Frequently used directories</string>
</property>
<layout class="QGridLayout" name="_5">
<item row="0" column="0">
<layout class="QHBoxLayout" name="_6">
<item>
<widget class="QListWidget" name="directory_list">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="iconSize">
<size>
<width>22</width>
<height>22</height>
</size>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="_7">
<item>
<spacer>
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="add_button">
<property name="toolTip">
<string>Add a directory to the frequently used directories list</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../../resources/images.qrc">
<normaloff>:/images/plus.svg</normaloff>:/images/plus.svg</iconset>
</property>
</widget>
</item>
<item>
<spacer>
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="remove_button">
<property name="toolTip">
<string>Remove a directory from the frequently used directories list</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../../resources/images.qrc">
<normaloff>:/images/list_remove.svg</normaloff>:/images/list_remove.svg</iconset>
</property>
</widget>
</item>
<item>
<spacer>
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<widget class="QWidget" name="page"> <widget class="QWidget" name="page">
@ -682,50 +585,43 @@
</layout> </layout>
</widget> </widget>
<widget class="QWidget" name="page_2"> <widget class="QWidget" name="page_2">
<layout class="QHBoxLayout" name="horizontalLayout_9"> <layout class="QGridLayout" name="gridLayout_3">
<item> <item row="0" column="0">
<spacer> <widget class="QLabel" name="label_5">
<property name="orientation"> <property name="text">
<enum>Qt::Horizontal</enum> <string>&amp;Maximum number of waiting worker processes (needs restart):</string>
</property> </property>
<property name="sizeHint" stdset="0"> <property name="buddy">
<size> <cstring>opt_worker_limit</cstring>
<width>154</width>
<height>20</height>
</size>
</property> </property>
</spacer> </widget>
</item> </item>
<item> <item row="0" column="1">
<layout class="QVBoxLayout" name="verticalLayout_12"> <widget class="QSpinBox" name="opt_worker_limit">
<item> <property name="minimum">
<widget class="QPushButton" name="compact_button"> <number>2</number>
<property name="text"> </property>
<string>&amp;Check database integrity</string> <property name="maximum">
</property> <number>10000</number>
</widget> </property>
</item> <property name="singleStep">
<item> <number>2</number>
<widget class="QPushButton" name="button_osx_symlinks"> </property>
<property name="text"> </widget>
<string>&amp;Install command line tools</string>
</property>
</widget>
</item>
</layout>
</item> </item>
<item> <item row="1" column="0" colspan="2">
<spacer> <widget class="QPushButton" name="compact_button">
<property name="orientation"> <property name="text">
<enum>Qt::Horizontal</enum> <string>&amp;Check database integrity</string>
</property> </property>
<property name="sizeHint" stdset="0"> </widget>
<size> </item>
<width>153</width> <item row="2" column="0" colspan="2">
<height>20</height> <widget class="QPushButton" name="button_osx_symlinks">
</size> <property name="text">
<string>&amp;Install command line tools</string>
</property> </property>
</spacer> </widget>
</item> </item>
</layout> </layout>
</widget> </widget>
@ -797,13 +693,6 @@
</widget> </widget>
</item> </item>
<item row="4" column="1"> <item row="4" column="1">
<widget class="QCheckBox" name="show_server_password">
<property name="text">
<string>&amp;Show password</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="max_cover_size"> <widget class="QLineEdit" name="max_cover_size">
<property name="toolTip"> <property name="toolTip">
<string>The maximum size (widthxheight) for displayed covers. Larger covers are resized. </string> <string>The maximum size (widthxheight) for displayed covers. Larger covers are resized. </string>
@ -813,7 +702,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="3" column="0"> <item row="4" column="0">
<widget class="QLabel" name="label_3"> <widget class="QLabel" name="label_3">
<property name="text"> <property name="text">
<string>Max. &amp;cover size:</string> <string>Max. &amp;cover size:</string>
@ -823,6 +712,33 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="3" column="1">
<widget class="QCheckBox" name="show_server_password">
<property name="text">
<string>&amp;Show password</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_15">
<property name="text">
<string>Max. &amp;OPDS items per query:</string>
</property>
<property name="buddy">
<cstring>opt_max_opds_items</cstring>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QSpinBox" name="opt_max_opds_items">
<property name="minimum">
<number>10</number>
</property>
<property name="maximum">
<number>10000</number>
</property>
</widget>
</item>
</layout> </layout>
</item> </item>
<item> <item>

View File

@ -14,7 +14,7 @@
<string>Active Jobs</string> <string>Active Jobs</string>
</property> </property>
<property name="windowIcon"> <property name="windowIcon">
<iconset resource="../../../../resources/images.qrc"> <iconset resource="../../../work/calibre/resources/images.qrc">
<normaloff>:/images/jobs.svg</normaloff>:/images/jobs.svg</iconset> <normaloff>:/images/jobs.svg</normaloff>:/images/jobs.svg</iconset>
</property> </property>
<layout class="QVBoxLayout"> <layout class="QVBoxLayout">
@ -57,6 +57,13 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="QPushButton" name="stop_all_jobs_button">
<property name="text">
<string>Stop &amp;all jobs</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<customwidgets> <customwidgets>
@ -67,7 +74,7 @@
</customwidget> </customwidget>
</customwidgets> </customwidgets>
<resources> <resources>
<include location="../../../../resources/images.qrc"/> <include location="../../../work/calibre/resources/images.qrc"/>
</resources> </resources>
<connections/> <connections/>
</ui> </ui>

View File

@ -252,9 +252,19 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
if ext not in extensions: if ext not in extensions:
self.db.remove_format(self.row, ext, notify=False) self.db.remove_format(self.row, ext, notify=False)
def __init__(self, window, row, db, accepted_callback=None): def do_cancel_all(self):
self.cancel_all = True
self.reject()
def __init__(self, window, row, db, accepted_callback=None, cancel_all=False):
ResizableDialog.__init__(self, window) ResizableDialog.__init__(self, window)
self.bc_box.layout().setAlignment(self.cover, Qt.AlignCenter|Qt.AlignHCenter) self.bc_box.layout().setAlignment(self.cover, Qt.AlignCenter|Qt.AlignHCenter)
self.cancel_all = False
if cancel_all:
self.__abort_button = self.button_box.addButton(self.button_box.Abort)
self.__abort_button.setToolTip(_('Abort the editing of all remaining books'))
self.connect(self.__abort_button, SIGNAL('clicked()'),
self.do_cancel_all)
self.splitter.setStretchFactor(100, 1) self.splitter.setStretchFactor(100, 1)
self.db = db self.db = db
self.pi = ProgressIndicator(self) self.pi = ProgressIndicator(self)

View File

@ -15,7 +15,7 @@ from PyQt4.Qt import QAbstractTableModel, QVariant, QModelIndex, Qt, \
from calibre.utils.ipc.server import Server from calibre.utils.ipc.server import Server
from calibre.utils.ipc.job import ParallelJob from calibre.utils.ipc.job import ParallelJob
from calibre.gui2 import Dispatcher, error_dialog, NONE from calibre.gui2 import Dispatcher, error_dialog, NONE, config
from calibre.gui2.device import DeviceJob from calibre.gui2.device import DeviceJob
from calibre.gui2.dialogs.jobs_ui import Ui_JobsDialog from calibre.gui2.dialogs.jobs_ui import Ui_JobsDialog
from calibre import __appname__ from calibre import __appname__
@ -31,7 +31,7 @@ class JobManager(QAbstractTableModel):
self.jobs = [] self.jobs = []
self.add_job = Dispatcher(self._add_job) self.add_job = Dispatcher(self._add_job)
self.server = Server() self.server = Server(limit=int(config['worker_limit']/2.0))
self.changed_queue = Queue() self.changed_queue = Queue()
self.timer = QTimer(self) self.timer = QTimer(self)
@ -193,6 +193,12 @@ class JobManager(QAbstractTableModel):
_('Job has already run')).exec_() _('Job has already run')).exec_()
self.server.kill_job(job) self.server.kill_job(job)
def kill_all_jobs(self):
for job in self.jobs:
if isinstance(job, DeviceJob) or job.duration is not None:
continue
self.server.kill_job(job)
def terminate_all_jobs(self): def terminate_all_jobs(self):
self.server.killall() self.server.killall()
@ -230,6 +236,8 @@ class JobsDialog(QDialog, Ui_JobsDialog):
self.kill_job) self.kill_job)
self.connect(self.details_button, SIGNAL('clicked()'), self.connect(self.details_button, SIGNAL('clicked()'),
self.show_details) self.show_details)
self.connect(self.stop_all_jobs_button, SIGNAL('clicked()'),
self.kill_all_jobs)
self.connect(self, SIGNAL('kill_job(int, PyQt_PyObject)'), self.connect(self, SIGNAL('kill_job(int, PyQt_PyObject)'),
self.jobs_view.model().kill_job) self.jobs_view.model().kill_job)
self.pb_delegate = ProgressBarDelegate(self) self.pb_delegate = ProgressBarDelegate(self)
@ -247,7 +255,8 @@ class JobsDialog(QDialog, Ui_JobsDialog):
self.jobs_view.show_details(index) self.jobs_view.show_details(index)
return return
def kill_all_jobs(self):
self.model.kill_all_jobs()
def closeEvent(self, e): def closeEvent(self, e):
self.jobs_view.write_settings() self.jobs_view.write_settings()

View File

@ -20,11 +20,12 @@ from calibre.constants import __version__, __appname__, \
from calibre.utils.filenames import ascii_filename from calibre.utils.filenames import ascii_filename
from calibre.ptempfile import PersistentTemporaryFile from calibre.ptempfile import PersistentTemporaryFile
from calibre.utils.config import prefs, dynamic from calibre.utils.config import prefs, dynamic
from calibre.utils.ipc import ADDRESS, RC
from calibre.utils.ipc.server import Server from calibre.utils.ipc.server import Server
from calibre.gui2 import APP_UID, warning_dialog, choose_files, error_dialog, \ from calibre.gui2 import APP_UID, warning_dialog, choose_files, error_dialog, \
initialize_file_icon_provider, question_dialog,\ initialize_file_icon_provider, question_dialog,\
pixmap_to_data, choose_dir, ORG_NAME, \ pixmap_to_data, choose_dir, ORG_NAME, \
set_sidebar_directories, Dispatcher, \ Dispatcher, \
Application, available_height, \ Application, available_height, \
max_available_height, config, info_dialog, \ max_available_height, config, info_dialog, \
available_width, GetMetadata available_width, GetMetadata
@ -50,9 +51,6 @@ from calibre.ebooks import BOOK_EXTENSIONS
from calibre.library.database2 import LibraryDatabase2, CoverCache from calibre.library.database2 import LibraryDatabase2, CoverCache
from calibre.gui2.dialogs.confirm_delete import confirm from calibre.gui2.dialogs.confirm_delete import confirm
ADDRESS = r'\\.\pipe\CalibreGUI' if iswindows else \
os.path.expanduser('~/.calibre-gui.socket')
class SaveMenu(QMenu): class SaveMenu(QMenu):
def __init__(self, parent): def __init__(self, parent):
@ -208,10 +206,11 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.latest_version = ' ' self.latest_version = ' '
self.vanity.setText(self.vanity_template%dict(version=' ', device=' ')) self.vanity.setText(self.vanity_template%dict(version=' ', device=' '))
self.device_info = ' ' self.device_info = ' '
self.update_checker = CheckForUpdates() if not opts.no_update_check:
QObject.connect(self.update_checker, self.update_checker = CheckForUpdates()
SIGNAL('update_found(PyQt_PyObject)'), self.update_found) QObject.connect(self.update_checker,
self.update_checker.start() SIGNAL('update_found(PyQt_PyObject)'), self.update_found)
self.update_checker.start()
####################### Status Bar ##################### ####################### Status Bar #####################
self.status_bar = StatusBar(self.jobs_dialog, self.system_tray_icon) self.status_bar = StatusBar(self.jobs_dialog, self.system_tray_icon)
self.setStatusBar(self.status_bar) self.setStatusBar(self.status_bar)
@ -531,7 +530,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self._sync_menu.trigger_default) self._sync_menu.trigger_default)
def add_spare_server(self, *args): def add_spare_server(self, *args):
self.spare_servers.append(Server()) self.spare_servers.append(Server(limit=int(config['worker_limit']/2.0)))
@property @property
def spare_server(self): def spare_server(self):
@ -1027,10 +1026,13 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self._metadata_view_id = self.library_view.model().db.id(row.row()) self._metadata_view_id = self.library_view.model().db.id(row.row())
d = MetadataSingleDialog(self, row.row(), d = MetadataSingleDialog(self, row.row(),
self.library_view.model().db, self.library_view.model().db,
accepted_callback=accepted) accepted_callback=accepted,
cancel_all=rows.index(row) < len(rows)-1)
self.connect(d, SIGNAL('view_format(PyQt_PyObject)'), self.connect(d, SIGNAL('view_format(PyQt_PyObject)'),
self.metadata_view_format) self.metadata_view_format)
d.exec_() d.exec_()
if d.cancel_all:
break
if rows: if rows:
current = self.library_view.currentIndex() current = self.library_view.currentIndex()
self.library_view.model().current_changed(current, previous) self.library_view.model().current_changed(current, previous)
@ -1484,8 +1486,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.save_menu.actions()[2].setText( self.save_menu.actions()[2].setText(
_('Save only %s format to disk')% _('Save only %s format to disk')%
prefs['output_format'].upper()) prefs['output_format'].upper())
if hasattr(d, 'directories'):
set_sidebar_directories(d.directories)
self.library_view.model().read_config() self.library_view.model().read_config()
self.create_device_menu() self.create_device_menu()
@ -1546,12 +1546,16 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.view_menu.actions()[1].setEnabled(True) self.view_menu.actions()[1].setEnabled(True)
self.action_open_containing_folder.setEnabled(True) self.action_open_containing_folder.setEnabled(True)
self.action_sync.setEnabled(True) self.action_sync.setEnabled(True)
self.status_bar.tag_view_button.setEnabled(True)
self.status_bar.cover_flow_button.setEnabled(True)
else: else:
self.action_edit.setEnabled(False) self.action_edit.setEnabled(False)
self.action_convert.setEnabled(False) self.action_convert.setEnabled(False)
self.view_menu.actions()[1].setEnabled(False) self.view_menu.actions()[1].setEnabled(False)
self.action_open_containing_folder.setEnabled(False) self.action_open_containing_folder.setEnabled(False)
self.action_sync.setEnabled(False) self.action_sync.setEnabled(False)
self.status_bar.tag_view_button.setEnabled(False)
self.status_bar.cover_flow_button.setEnabled(False)
def device_job_exception(self, job): def device_job_exception(self, job):
@ -1644,7 +1648,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
geometry = config['main_window_geometry'] geometry = config['main_window_geometry']
if geometry is not None: if geometry is not None:
self.restoreGeometry(geometry) self.restoreGeometry(geometry)
set_sidebar_directories(None)
self.tool_bar.setIconSize(config['toolbar_icon_size']) self.tool_bar.setIconSize(config['toolbar_icon_size'])
self.tool_bar.setToolButtonStyle( self.tool_bar.setToolButtonStyle(
Qt.ToolButtonTextUnderIcon if \ Qt.ToolButtonTextUnderIcon if \
@ -1814,6 +1817,8 @@ path_to_ebook to the database.
help=_('Start minimized to system tray.')) help=_('Start minimized to system tray.'))
parser.add_option('-v', '--verbose', default=0, action='count', parser.add_option('-v', '--verbose', default=0, action='count',
help=_('Log debugging information to console')) help=_('Log debugging information to console'))
parser.add_option('--no-update-check', default=False, action='store_true',
help=_('Do not check for updates'))
return parser return parser
def init_qt(args): def init_qt(args):
@ -1882,14 +1887,6 @@ def cant_start(msg=_('If you are sure it is not running')+', ',
d.exec_() d.exec_()
raise SystemExit(1) raise SystemExit(1)
class RC(Thread):
def run(self):
from multiprocessing.connection import Client
self.done = False
self.conn = Client(ADDRESS)
self.done = True
def communicate(args): def communicate(args):
t = RC() t = RC()
t.start() t.start()

View File

@ -36,6 +36,7 @@ class SearchBox2(QComboBox):
def __init__(self, parent=None): def __init__(self, parent=None):
QComboBox.__init__(self, parent) QComboBox.__init__(self, parent)
self.normal_background = 'rgb(255, 255, 255, 0%)'
self.line_edit = SearchLineEdit(self) self.line_edit = SearchLineEdit(self)
self.setLineEdit(self.line_edit) self.setLineEdit(self.line_edit)
self.connect(self.line_edit, SIGNAL('key_pressed(PyQt_PyObject)'), self.connect(self.line_edit, SIGNAL('key_pressed(PyQt_PyObject)'),
@ -62,14 +63,18 @@ class SearchBox2(QComboBox):
def normalize_state(self): def normalize_state(self):
self.setEditText('') self.setEditText('')
self.line_edit.setStyleSheet('QLineEdit { color: black; background-color: white; }') self.line_edit.setStyleSheet(
'QLineEdit { color: black; background-color: %s; }' %
self.normal_background)
self.help_state = False self.help_state = False
def clear_to_help(self): def clear_to_help(self):
self.setEditText(self.help_text) self.setEditText(self.help_text)
self.line_edit.home(False) self.line_edit.home(False)
self.help_state = True self.help_state = True
self.line_edit.setStyleSheet('QLineEdit { color: gray; background-color: white; }') self.line_edit.setStyleSheet(
'QLineEdit { color: gray; background-color: %s; }' %
self.normal_background)
self.emit(SIGNAL('cleared()')) self.emit(SIGNAL('cleared()'))
def text(self): def text(self):
@ -84,7 +89,7 @@ class SearchBox2(QComboBox):
return self.clear_to_help() return self.clear_to_help()
col = 'rgba(0,255,0,20%)' if ok else 'rgb(255,0,0,20%)' col = 'rgba(0,255,0,20%)' if ok else 'rgb(255,0,0,20%)'
if not self.colorize: if not self.colorize:
col = 'white' col = self.normal_background
self.line_edit.setStyleSheet('QLineEdit { color: black; background-color: %s; }' % col) self.line_edit.setStyleSheet('QLineEdit { color: black; background-color: %s; }' % col)
def key_pressed(self, event): def key_pressed(self, event):

View File

@ -14,8 +14,10 @@ from PyQt4.Qt import QDialog
from calibre.ptempfile import PersistentTemporaryFile from calibre.ptempfile import PersistentTemporaryFile
from calibre.gui2 import warning_dialog, question_dialog from calibre.gui2 import warning_dialog, question_dialog
from calibre.gui2.convert.single import NoSupportedInputFormats from calibre.gui2.convert.single import NoSupportedInputFormats
from calibre.gui2.convert.single import Config as SingleConfig from calibre.gui2.convert.single import Config as SingleConfig, \
get_input_format_for_book
from calibre.gui2.convert.bulk import BulkConfig from calibre.gui2.convert.bulk import BulkConfig
from calibre.gui2.convert.metadata import create_opf_file, create_cover_file
from calibre.customize.conversion import OptionRecommendation from calibre.customize.conversion import OptionRecommendation
from calibre.utils.config import prefs from calibre.utils.config import prefs
from calibre.ebooks.conversion.config import GuiRecommendations, \ from calibre.ebooks.conversion.config import GuiRecommendations, \
@ -55,7 +57,11 @@ def convert_single_ebook(parent, db, book_ids, auto_conversion=False, out_format
out_file.close() out_file.close()
temp_files = [] temp_files = []
desc = _('Convert book %d of %d (%s)') % (i + 1, total, repr(mi.title)) try:
dtitle = unicode(mi.title)
except:
dtitle = repr(mi.title)
desc = _('Convert book %d of %d (%s)') % (i + 1, total, dtitle)
recs = cPickle.loads(d.recommendations) recs = cPickle.loads(d.recommendations)
if d.opf_file is not None: if d.opf_file is not None:
@ -108,13 +114,11 @@ def convert_bulk_ebook(parent, db, book_ids, out_format=None):
book_ids = convert_existing(parent, db, book_ids, output_format) book_ids = convert_existing(parent, db, book_ids, output_format)
for i, book_id in enumerate(book_ids): for i, book_id in enumerate(book_ids):
temp_files = [] temp_files = []
input_format = get_input_format_for_book(db, book_id, None)[0]
try: try:
d = SingleConfig(parent, db, book_id, None, output_format) mi, opf_file = create_opf_file(db, book_id)
d.accept() in_file = db.format_abspath(book_id, input_format, True)
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 = PersistentTemporaryFile('.' + output_format)
out_file.write(output_format) out_file.write(output_format)
@ -122,7 +126,7 @@ def convert_bulk_ebook(parent, db, book_ids, out_format=None):
temp_files = [] temp_files = []
combined_recs = GuiRecommendations() combined_recs = GuiRecommendations()
default_recs = load_defaults('%s_input' % d.input_format) default_recs = load_defaults('%s_input' % input_format)
specific_recs = load_specifics(db, book_id) specific_recs = load_specifics(db, book_id)
for key in default_recs: for key in default_recs:
combined_recs[key] = default_recs[key] combined_recs[key] = default_recs[key]
@ -133,24 +137,30 @@ def convert_bulk_ebook(parent, db, book_ids, out_format=None):
save_specifics(db, book_id, combined_recs) save_specifics(db, book_id, combined_recs)
lrecs = list(combined_recs.to_recommendations()) lrecs = list(combined_recs.to_recommendations())
if d.opf_file is not None: cover_file = create_cover_file(db, book_id)
lrecs.append(('read_metadata_from_opf', d.opf_file.name,
if opf_file is not None:
lrecs.append(('read_metadata_from_opf', opf_file.name,
OptionRecommendation.HIGH)) OptionRecommendation.HIGH))
temp_files.append(d.opf_file) temp_files.append(opf_file)
if d.cover_file is not None: if cover_file is not None:
lrecs.append(('cover', d.cover_file.name, lrecs.append(('cover', cover_file.name,
OptionRecommendation.HIGH)) OptionRecommendation.HIGH))
temp_files.append(d.cover_file) temp_files.append(cover_file)
for x in list(lrecs): for x in list(lrecs):
if x[0] == 'debug_pipeline': if x[0] == 'debug_pipeline':
lrecs.remove(x) lrecs.remove(x)
desc = _('Convert book %d of %d (%s)') % (i + 1, total, repr(mi.title)) try:
dtitle = unicode(mi.title)
except:
dtitle = repr(mi.title)
desc = _('Convert book %d of %d (%s)') % (i + 1, total, dtitle)
args = [in_file, out_file.name, lrecs] args = [in_file, out_file.name, lrecs]
temp_files.append(out_file) temp_files.append(out_file)
jobs.append(('gui_convert', args, desc, d.output_format.upper(), book_id, temp_files)) jobs.append(('gui_convert', args, desc, output_format.upper(), book_id, temp_files))
changed = True changed = True
except NoSupportedInputFormats: except NoSupportedInputFormats:
@ -168,6 +178,7 @@ def convert_bulk_ebook(parent, db, book_ids, out_format=None):
'source format was found.') % (len(res), total), 'source format was found.') % (len(res), total),
msg).exec_() msg).exec_()
jobs.reverse()
return jobs, changed, bad return jobs, changed, bad
def fetch_scheduled_recipe(recipe, script): def fetch_scheduled_recipe(recipe, script):

View File

@ -22,4 +22,7 @@ def server_config(defaults=None):
help='Development mode. Server automatically restarts on file changes and serves code files (html, css, js) from the file system instead of calibre\'s resource system.') help='Development mode. Server automatically restarts on file changes and serves code files (html, css, js) from the file system instead of calibre\'s resource system.')
c.add_opt('max_cover', ['--max-cover'], default='600x800', c.add_opt('max_cover', ['--max-cover'], default='600x800',
help=_('The maximum size for displayed covers. Default is %default.')) help=_('The maximum size for displayed covers. Default is %default.'))
c.add_opt('max_opds_items', ['--max-opds-items'], default=30,
help=_('The maximum number of matches to return per OPDS query. '
'This affects Stanza, WordPlayer, etc. integration.'))
return c return c

View File

@ -13,11 +13,6 @@ from urllib import quote
from calibre import terminal_controller, preferred_encoding, prints from calibre import terminal_controller, preferred_encoding, prints
from calibre.utils.config import OptionParser, prefs from calibre.utils.config import OptionParser, prefs
try:
from calibre.utils.single_qt_application import send_message
send_message
except:
send_message = None
from calibre.ebooks.metadata.meta import get_metadata from calibre.ebooks.metadata.meta import get_metadata
from calibre.library.database2 import LibraryDatabase2 from calibre.library.database2 import LibraryDatabase2
from calibre.ebooks.metadata.opf2 import OPFCreator, OPF from calibre.ebooks.metadata.opf2 import OPFCreator, OPF
@ -102,6 +97,20 @@ STANZA_TEMPLATE='''\
</feed> </feed>
''' '''
def send_message(msg=''):
prints('Notifying calibre of the change')
from calibre.utils.ipc import RC
import time
t = RC(print_error=False)
t.start()
time.sleep(3)
if t.done:
t.conn.send('refreshdb:'+msg)
t.conn.close()
def get_parser(usage): def get_parser(usage):
parser = OptionParser(usage) parser = OptionParser(usage)
go = parser.add_option_group('GLOBAL OPTIONS') go = parser.add_option_group('GLOBAL OPTIONS')
@ -314,8 +323,7 @@ def do_add(db, paths, one_book_per_directory, recurse, add_duplicates):
print >>sys.stderr, '\t', title+':' print >>sys.stderr, '\t', title+':'
print >>sys.stderr, '\t\t ', path print >>sys.stderr, '\t\t ', path
if send_message is not None: send_message()
send_message('refreshdb:', 'calibre GUI')
finally: finally:
sys.stdout = orig sys.stdout = orig
@ -356,8 +364,7 @@ def do_remove(db, ids):
for y in x: for y in x:
db.delete_book(y) db.delete_book(y)
if send_message is not None: send_message()
send_message('refreshdb:', 'calibre GUI')
def remove_option_parser(): def remove_option_parser():
return get_parser(_( return get_parser(_(
@ -386,7 +393,7 @@ def command_remove(args, dbpath):
else: else:
ids.append(int(y[0])) ids.append(int(y[0]))
do_remove(get_db(dbpath, opts), ids) do_remove(get_db(dbpath, opts), set(ids))
return 0 return 0
@ -484,8 +491,7 @@ def do_set_metadata(db, id, stream):
mi = OPF(stream) mi = OPF(stream)
db.set_metadata(id, mi) db.set_metadata(id, mi)
do_show_metadata(db, id, False) do_show_metadata(db, id, False)
if send_message is not None: send_message()
send_message('refreshdb:', 'calibre GUI')
def set_metadata_option_parser(): def set_metadata_option_parser():
return get_parser(_( return get_parser(_(

View File

@ -1047,6 +1047,18 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
return [ (i[0], i[1]) for i in \ return [ (i[0], i[1]) for i in \
self.conn.get('SELECT id, name FROM series')] self.conn.get('SELECT id, name FROM series')]
def series_name(self, series_id):
return self.conn.get('SELECT name FROM series WHERE id=%d'%series_id,
all=False)
def author_name(self, author_id):
return self.conn.get('SELECT name FROM authors WHERE id=%d'%author_id,
all=False)
def tag_name(self, tag_id):
return self.conn.get('SELECT name FROM tags WHERE id=%d'%tag_id,
all=False)
def all_authors(self): def all_authors(self):
return [ (i[0], i[1]) for i in \ return [ (i[0], i[1]) for i in \
self.conn.get('SELECT id, name FROM authors')] self.conn.get('SELECT id, name FROM authors')]
@ -1058,6 +1070,11 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
def all_tags(self): def all_tags(self):
return [i[0].strip() for i in self.conn.get('SELECT name FROM tags') if i[0].strip()] return [i[0].strip() for i in self.conn.get('SELECT name FROM tags') if i[0].strip()]
def all_tags2(self):
return [ (i[0], i[1]) for i in \
self.conn.get('SELECT id, name FROM tags')]
def conversion_options(self, id, format): def conversion_options(self, id, format):
data = self.conn.get('SELECT data FROM conversion_options WHERE book=? AND format=?', (id, format.upper()), all=False) data = self.conn.get('SELECT data FROM conversion_options WHERE book=? AND format=?', (id, format.upper()), all=False)
if data: if data:

View File

@ -921,9 +921,12 @@ class LibraryDatabase2(LibraryDatabase):
''' '''
Removes book from the result cache and the underlying database. Removes book from the result cache and the underlying database.
''' '''
path = os.path.join(self.library_path, self.path(id, index_is_id=True)) try:
path = os.path.join(self.library_path, self.path(id, index_is_id=True))
except:
path = None
self.data.remove(id) self.data.remove(id)
if os.path.exists(path): if path and os.path.exists(path):
try: try:
winshell.delete_file(path, no_confirm=True, silent=True) winshell.delete_file(path, no_confirm=True, silent=True)
except: except:

View File

@ -101,30 +101,13 @@ class LibraryServer(object):
</entry> </entry>
''')) '''))
STANZA_AUTHOR_ENTRY=MarkupTemplate(textwrap.dedent('''\ STANZA_SUBCATALOG_ENTRY=MarkupTemplate(textwrap.dedent('''\
<entry xmlns:py="http://genshi.edgewall.org/"> <entry xmlns:py="http://genshi.edgewall.org/">
<title>${authors}</title> <title>${title}</title>
<id>urn:calibre:${record[FM['id']]}</id> <id>urn:calibre:${id}</id>
<updated>${timestamp}</updated> <updated>${updated.strftime('%Y-%m-%dT%H:%M:%S+00:00')}</updated>
<link type="application/atom+xml" href="/stanza/?authorid=${record[FM['id']]}" /> <link type="application/atom+xml" href="/stanza/?${what}id=${id}" />
</entry> <content type="text">${count} books</content>
'''))
STANZA_TAG_ENTRY=MarkupTemplate(textwrap.dedent('''\
<entry xmlns:py="http://genshi.edgewall.org/">
<title>${tags}</title>
<id>urn:calibre:${record[FM['id']]}</id>
<updated>${timestamp}</updated>
<link type="application/atom+xml" href="/stanza/?tagid=${record[FM['id']]}" />
</entry>
'''))
STANZA_SERIES_ENTRY=MarkupTemplate(textwrap.dedent('''\
<entry xmlns:py="http://genshi.edgewall.org/">
<title>${series}</title>
<id>urn:calibre:${record[FM['id']]}</id>
<updated>${timestamp}</updated>
<link type="application/atom+xml" href="/stanza/?seriesid=${record[FM['id']]}" />
</entry> </entry>
''')) '''))
@ -135,6 +118,7 @@ class LibraryServer(object):
<id>$id</id> <id>$id</id>
<updated>${updated.strftime('%Y-%m-%dT%H:%M:%S+00:00')}</updated> <updated>${updated.strftime('%Y-%m-%dT%H:%M:%S+00:00')}</updated>
<link rel="search" title="Search" type="application/atom+xml" href="/stanza/?search={searchTerms}"/> <link rel="search" title="Search" type="application/atom+xml" href="/stanza/?search={searchTerms}"/>
${Markup(next_link)}
<author> <author>
<name>calibre</name> <name>calibre</name>
<uri>http://calibre.kovidgoyal.net</uri> <uri>http://calibre.kovidgoyal.net</uri>
@ -167,30 +151,35 @@ class LibraryServer(object):
<id>urn:uuid:fc000fa0-8c23-11de-a31d-0002a5d5c51b</id> <id>urn:uuid:fc000fa0-8c23-11de-a31d-0002a5d5c51b</id>
<updated>${updated.strftime('%Y-%m-%dT%H:%M:%S+00:00')}</updated> <updated>${updated.strftime('%Y-%m-%dT%H:%M:%S+00:00')}</updated>
<link type="application/atom+xml" href="/stanza/?sortby=byauthor" /> <link type="application/atom+xml" href="/stanza/?sortby=byauthor" />
<content type="text">Books sorted by Author</content>
</entry> </entry>
<entry> <entry>
<title>By Title</title> <title>By Title</title>
<id>urn:uuid:1df4fe40-8c24-11de-b4c6-0002a5d5c51b</id> <id>urn:uuid:1df4fe40-8c24-11de-b4c6-0002a5d5c51b</id>
<updated>${updated.strftime('%Y-%m-%dT%H:%M:%S+00:00')}</updated> <updated>${updated.strftime('%Y-%m-%dT%H:%M:%S+00:00')}</updated>
<link type="application/atom+xml" href="/stanza/?sortby=bytitle" /> <link type="application/atom+xml" href="/stanza/?sortby=bytitle" />
<content type="text">Books sorted by Title</content>
</entry> </entry>
<entry> <entry>
<title>By Newest</title> <title>By Newest</title>
<id>urn:uuid:3c6d4940-8c24-11de-a4d7-0002a5d5c51b</id> <id>urn:uuid:3c6d4940-8c24-11de-a4d7-0002a5d5c51b</id>
<updated>${updated.strftime('%Y-%m-%dT%H:%M:%S+00:00')}</updated> <updated>${updated.strftime('%Y-%m-%dT%H:%M:%S+00:00')}</updated>
<link type="application/atom+xml" href="/stanza/?sortby=bynewest" /> <link type="application/atom+xml" href="/stanza/?sortby=bynewest" />
<content type="text">Books sorted by Date</content>
</entry> </entry>
<entry> <entry>
<title>By Tag</title> <title>By Tag</title>
<id>urn:uuid:824921e8-db8a-4e61-7d38-f1ce41502853</id> <id>urn:uuid:824921e8-db8a-4e61-7d38-f1ce41502853</id>
<updated>${updated.strftime('%Y-%m-%dT%H:%M:%S+00:00')}</updated> <updated>${updated.strftime('%Y-%m-%dT%H:%M:%S+00:00')}</updated>
<link type="application/atom+xml" href="/stanza/?sortby=bytag" /> <link type="application/atom+xml" href="/stanza/?sortby=bytag" />
<content type="text">Books sorted by Tags</content>
</entry> </entry>
<entry> <entry>
<title>By Series</title> <title>By Series</title>
<id>urn:uuid:512a5e50-a88f-f6b8-82aa-8f129c719f61</id> <id>urn:uuid:512a5e50-a88f-f6b8-82aa-8f129c719f61</id>
<updated>${updated.strftime('%Y-%m-%dT%H:%M:%S+00:00')}</updated> <updated>${updated.strftime('%Y-%m-%dT%H:%M:%S+00:00')}</updated>
<link type="application/atom+xml" href="/stanza/?sortby=byseries" /> <link type="application/atom+xml" href="/stanza/?sortby=byseries" />
<content type="text">Books sorted by Series</content>
</entry> </entry>
</feed> </feed>
''')) '''))
@ -204,6 +193,7 @@ class LibraryServer(object):
self.opts = opts self.opts = opts
self.max_cover_width, self.max_cover_height = \ self.max_cover_width, self.max_cover_height = \
map(int, self.opts.max_cover.split('x')) map(int, self.opts.max_cover.split('x'))
self.max_stanza_items = opts.max_opds_items
path = P('content_server') path = P('content_server')
self.build_time = datetime.fromtimestamp(os.stat(path).st_mtime) self.build_time = datetime.fromtimestamp(os.stat(path).st_mtime)
self.default_cover = open(P('content_server/default_cover.jpg'), 'rb').read() self.default_cover = open(P('content_server/default_cover.jpg'), 'rb').read()
@ -281,6 +271,7 @@ class LibraryServer(object):
if cover is None: if cover is None:
cover = self.default_cover cover = self.default_cover
cherrypy.response.headers['Content-Type'] = 'image/jpeg' cherrypy.response.headers['Content-Type'] = 'image/jpeg'
cherrypy.response.timeout = 3600
path = getattr(cover, 'name', False) path = getattr(cover, 'name', False)
updated = datetime.utcfromtimestamp(os.stat(path).st_mtime) if path and \ updated = datetime.utcfromtimestamp(os.stat(path).st_mtime) if path and \
os.access(path, os.R_OK) else self.build_time os.access(path, os.R_OK) else self.build_time
@ -326,6 +317,7 @@ class LibraryServer(object):
if mt is None: if mt is None:
mt = 'application/octet-stream' mt = 'application/octet-stream'
cherrypy.response.headers['Content-Type'] = mt cherrypy.response.headers['Content-Type'] = mt
cherrypy.response.timeout = 3600
path = getattr(fmt, 'name', None) path = getattr(fmt, 'name', None)
if path and os.path.exists(path): if path and os.path.exists(path):
updated = datetime.utcfromtimestamp(os.stat(path).st_mtime) updated = datetime.utcfromtimestamp(os.stat(path).st_mtime)
@ -367,123 +359,162 @@ class LibraryServer(object):
8:'Aug', 9:'Sep', 10:'Oct', 11:'Nov', 12:'Dec'} 8:'Aug', 9:'Sep', 10:'Oct', 11:'Nov', 12:'Dec'}
return lm.replace('month', month[updated.month]) return lm.replace('month', month[updated.month])
def get_matches(self, location, query):
base = self.db.data.get_matches(location, query)
epub = self.db.data.get_matches('format', 'epub')
pdb = self.db.data.get_matches('format', 'pdb')
return base.intersection(epub.union(pdb))
def stanza_sortby_subcategory(self, updated, sortby, offset):
what, subtitle = sortby[2:], ''
if sortby == 'byseries':
data = self.db.all_series()
data = [(x[0], x[1], len(self.get_matches('series', x[1]))) for x in data]
subtitle = 'Books by series'
elif sortby == 'byauthor':
data = self.db.all_authors()
data = [(x[0], x[1], len(self.get_matches('authors', x[1]))) for x in data]
subtitle = 'Books by author'
elif sortby == 'bytag':
data = self.db.all_tags2()
data = [(x[0], x[1], len(self.get_matches('tags', x[1]))) for x in data]
subtitle = 'Books by tag'
data = [x for x in data if x[2] > 0]
data.sort(cmp=lambda x, y: cmp(x[1], y[1]))
next_offset = offset + self.max_stanza_items
rdata = data[offset:next_offset]
if next_offset >= len(data):
next_offset = -1
entries = [self.STANZA_SUBCATALOG_ENTRY.generate(title=title, id=id,
what=what, updated=updated, count=c).render('xml').decode('utf-8') for id,
title, c in rdata]
next_link = ''
if next_offset > -1:
next_link = ('<link rel="next" title="Next" '
'type="application/atom+xml" href="/stanza/?sortby=%s&amp;offset=%d"/>\n'
) % (sortby, next_offset)
return self.STANZA.generate(subtitle=subtitle, data=entries, FM=FIELD_MAP,
updated=updated, id='urn:calibre:main', next_link=next_link).render('xml')
def stanza_main(self, updated):
return self.STANZA_MAIN.generate(subtitle='', data=[], FM=FIELD_MAP,
updated=updated, id='urn:calibre:main').render('xml')
@expose @expose
def stanza(self, search=None, sortby=None, authorid=None, tagid=None, seriesid=None): def stanza(self, search=None, sortby=None, authorid=None, tagid=None,
seriesid=None, offset=0):
'Feeds to read calibre books on a ipod with stanza.' 'Feeds to read calibre books on a ipod with stanza.'
books = [] books = []
updated = self.db.last_modified() updated = self.db.last_modified()
offset = int(offset)
cherrypy.response.headers['Last-Modified'] = self.last_modified(updated) cherrypy.response.headers['Last-Modified'] = self.last_modified(updated)
cherrypy.response.headers['Content-Type'] = 'text/xml' cherrypy.response.headers['Content-Type'] = 'text/xml'
# Main feed
if not sortby and not search and not authorid and not tagid and not seriesid: if not sortby and not search and not authorid and not tagid and not seriesid:
return self.STANZA_MAIN.generate(subtitle='', data=books, FM=FIELD_MAP, return self.stanza_main(updated)
updated=updated, id='urn:calibre:main').render('xml') if sortby in ('byseries', 'byauthor', 'bytag'):
return self.stanza_sortby_subcategory(updated, sortby, offset)
# Get matching ids
if authorid: if authorid:
authorid=int(authorid) authorid=int(authorid)
au = self.db.authors(authorid, index_is_id=True) au = self.db.author_name(authorid)
ids = self.db.data.get_matches('authors', au) ids = self.get_matches('authors', au)
elif tagid: elif tagid:
tagid=int(tagid) tagid=int(tagid)
ta = self.db.tags(tagid, index_is_id=True) ta = self.db.tag_name(tagid)
ids = self.db.data.get_matches('tags', ta) ids = self.get_matches('tags', ta)
elif seriesid: elif seriesid:
seriesid=int(seriesid) seriesid=int(seriesid)
se = self.db.series(seriesid, index_is_id=True) se = self.db.series_name(seriesid)
ids = self.db.data.get_matches('series', se) ids = self.get_matches('series', se)
else: else:
ids = self.db.data.parse(search) if search and search.strip() else self.db.data.universal_set() ids = self.db.data.parse(search) if search and search.strip() else self.db.data.universal_set()
record_list = list(iter(self.db)) record_list = list(iter(self.db))
if sortby == "byauthor":
record_list.sort(lambda x, y: cmp(x[FIELD_MAP['author_sort']], y[FIELD_MAP['author_sort']])) # Sort the record list
elif sortby == "bytag": if sortby == "bytitle" or authorid or tagid:
record_list.sort(lambda x, y: cmp(x[FIELD_MAP['tags']], y[FIELD_MAP['tags']]))
elif sortby == "byseries":
record_list.sort(lambda x, y: cmp(x[FIELD_MAP['series']], y[FIELD_MAP['series']]))
elif sortby == "bytitle" or authorid or tagid:
record_list.sort(lambda x, y: cmp(title_sort(x[FIELD_MAP['title']]), record_list.sort(lambda x, y: cmp(title_sort(x[FIELD_MAP['title']]),
title_sort(y[FIELD_MAP['title']]))) title_sort(y[FIELD_MAP['title']])))
elif seriesid: elif seriesid:
record_list.sort(lambda x, y: cmp(x[FIELD_MAP['series_index']], y[FIELD_MAP['series_index']])) record_list.sort(lambda x, y: cmp(x[FIELD_MAP['series_index']], y[FIELD_MAP['series_index']]))
else: else: # Sort by date
record_list = reversed(record_list) record_list = reversed(record_list)
fmts = FIELD_MAP['formats']
pat = re.compile(r'EPUB|PDB', re.IGNORECASE)
record_list = [x for x in record_list if x[0] in ids and
pat.search(x[fmts] if x[fmts] else '') is not None]
next_offset = offset + self.max_stanza_items
nrecord_list = record_list[offset:next_offset]
if next_offset >= len(record_list):
next_offset = -1
next_link = ''
if next_offset > -1:
q = ['offset=%d'%next_offset]
for x in ('search', 'sortby', 'authorid', 'tagid', 'seriesid'):
val = locals()[x]
if val is not None:
val = prepare_string_for_xml(unicode(val), True)
q.append('%s=%s'%(x, val))
next_link = ('<link rel="next" title="Next" '
'type="application/atom+xml" href="/stanza/?%s"/>\n'
) % '&amp;'.join(q)
author_list=[] author_list=[]
tag_list=[] tag_list=[]
series_list=[] series_list=[]
for record in record_list:
if record[0] not in ids: continue for record in nrecord_list:
r = record[FIELD_MAP['formats']] r = record[FIELD_MAP['formats']]
r = r.upper() if r else '' r = r.upper() if r else ''
if 'EPUB' in r or 'PDB' in r:
z = record[FIELD_MAP['authors']] z = record[FIELD_MAP['authors']]
if not z: if not z:
z = _('Unknown') z = _('Unknown')
authors = ' & '.join([i.replace('|', ',') for i in authors = ' & '.join([i.replace('|', ',') for i in
z.split(',')]) z.split(',')])
extra = []
rating = record[FIELD_MAP['rating']] # Setup extra description
if rating > 0: extra = []
rating = ''.join(repeat('&#9733;', rating)) rating = record[FIELD_MAP['rating']]
extra.append('RATING: %s<br />'%rating) if rating > 0:
tags = record[FIELD_MAP['tags']] rating = ''.join(repeat('&#9733;', rating))
if tags: extra.append('RATING: %s<br />'%rating)
extra.append('TAGS: %s<br />'%\ tags = record[FIELD_MAP['tags']]
prepare_string_for_xml(', '.join(tags.split(',')))) if tags:
series = record[FIELD_MAP['series']] extra.append('TAGS: %s<br />'%\
if series: prepare_string_for_xml(', '.join(tags.split(','))))
extra.append('SERIES: %s [%s]<br />'%\ series = record[FIELD_MAP['series']]
(prepare_string_for_xml(series), if series:
fmt_sidx(float(record[FIELD_MAP['series_index']])))) extra.append('SERIES: %s [%s]<br />'%\
fmt = 'epub' if 'EPUB' in r else 'pdb' (prepare_string_for_xml(series),
mimetype = guess_type('dummy.'+fmt)[0] fmt_sidx(float(record[FIELD_MAP['series_index']]))))
if sortby == "byauthor":
if authors and authors not in author_list: fmt = 'epub' if 'EPUB' in r else 'pdb'
author_list.append(authors) mimetype = guess_type('dummy.'+fmt)[0]
books.append(self.STANZA_AUTHOR_ENTRY.generate(
authors=authors, # Create the sub-catalog, which is either a list of
record=record, FM=FIELD_MAP, # authors/tags/series or a list of books
port=self.opts.port, data = dict(
extra=''.join(extra), record=record,
mimetype=mimetype, updated=updated,
fmt=fmt, authors=authors,
timestamp=strftime('%Y-%m-%dT%H:%M:%S+00:00', record[5]), tags=tags,
).render('xml').decode('utf8')) series=series,
elif sortby == "bytag": FM=FIELD_MAP,
if tags and tags not in tag_list: extra='\n'.join(extra),
tag_list.append(tags) mimetype=mimetype,
books.append(self.STANZA_TAG_ENTRY.generate( fmt=fmt,
tags=tags, timestamp=strftime('%Y-%m-%dT%H:%M:%S+00:00', record[5])
record=record, FM=FIELD_MAP, )
port=self.opts.port, books.append(self.STANZA_ENTRY.generate(**data)\
extra=''.join(extra), .render('xml').decode('utf8'))
mimetype=mimetype,
fmt=fmt,
timestamp=strftime('%Y-%m-%dT%H:%M:%S+00:00', record[5]),
).render('xml').decode('utf8'))
elif sortby == "byseries":
if series and series not in series_list:
series_list.append(series)
books.append(self.STANZA_SERIES_ENTRY.generate(
series=series,
record=record, FM=FIELD_MAP,
port=self.opts.port,
extra=''.join(extra),
mimetype=mimetype,
fmt=fmt,
timestamp=strftime('%Y-%m-%dT%H:%M:%S+00:00', record[5]),
).render('xml').decode('utf8'))
else:
books.append(self.STANZA_ENTRY.generate(
authors=authors,
record=record, FM=FIELD_MAP,
port=self.opts.port,
extra=''.join(extra),
mimetype=mimetype,
fmt=fmt,
timestamp=strftime('%Y-%m-%dT%H:%M:%S+00:00', record[5]),
).render('xml').decode('utf8'))
return self.STANZA.generate(subtitle='', data=books, FM=FIELD_MAP, return self.STANZA.generate(subtitle='', data=books, FM=FIELD_MAP,
updated=updated, id='urn:calibre:main').render('xml') next_link=next_link, updated=updated, id='urn:calibre:main').render('xml')
@expose @expose
@ -537,7 +568,9 @@ class LibraryServer(object):
cherrypy.request.headers.get('Want-OPDS-Catalog', 919) != 919 or \ cherrypy.request.headers.get('Want-OPDS-Catalog', 919) != 919 or \
ua.startswith('Stanza') ua.startswith('Stanza')
return self.stanza(search=kwargs.get('search', None), sortby=kwargs.get('sortby',None), authorid=kwargs.get('authorid',None), return self.stanza(search=kwargs.get('search', None), sortby=kwargs.get('sortby',None), authorid=kwargs.get('authorid',None),
tagid=kwargs.get('tagid',None), seriesid=kwargs.get('seriesid',None)) if want_opds else self.static('index.html') tagid=kwargs.get('tagid',None),
seriesid=kwargs.get('seriesid',None),
offset=kwargs.get('offset', 0)) if want_opds else self.static('index.html')
@expose @expose

View File

@ -22,6 +22,22 @@ _run_once = False
if not _run_once: if not _run_once:
_run_once = True _run_once = True
################################################################################
# Platform specific modules
winutil = winutilerror = None
if iswindows:
winutil, winutilerror = plugins['winutil']
if not winutil:
raise RuntimeError('Failed to load the winutil plugin: %s'%winutilerror)
if len(sys.argv) > 1:
sys.argv[1:] = winutil.argv()[1-len(sys.argv):]
################################################################################
# Convert command line arguments to unicode
for i in range(1, len(sys.argv)):
if not isinstance(sys.argv[i], unicode):
sys.argv[i] = sys.argv[i].decode(preferred_encoding, 'replace')
################################################################################ ################################################################################
# Setup resources # Setup resources
import calibre.utils.resources as resources import calibre.utils.resources as resources
@ -89,18 +105,4 @@ if not _run_once:
os.path.join = my_join os.path.join = my_join
################################################################################
# Platform specific modules
winutil = winutilerror = None
if iswindows:
winutil, winutilerror = plugins['winutil']
if not winutil:
raise RuntimeError('Failed to load the winutil plugin: %s'%winutilerror)
if len(sys.argv) > 1:
sys.argv[1:] = winutil.argv()[1-len(sys.argv):]
################################################################################
# Convert command line arguments to unicode
for i in range(1, len(sys.argv)):
if not isinstance(sys.argv[i], unicode):
sys.argv[i] = sys.argv[i].decode(preferred_encoding, 'replace')

View File

@ -148,6 +148,11 @@ else:
path=MOBILEREAD+file, app=__appname__, path=MOBILEREAD+file, app=__appname__,
note=Markup(\ note=Markup(\
''' '''
<p>If you are updating from a version of calibre older than 0.6.12 on
<b>Windows XP</b>, first uninstall calibre, then delete the C:\Program
Files\calibre folder (the location may be different if you previously
installed calibre elsewhere) and only then install the new version of
calibre.</p><p><br /></p>
<p>If you are using the <b>SONY PRS-500</b> and %(appname)s does not detect your reader, read on:</p> <p>If you are using the <b>SONY PRS-500</b> and %(appname)s does not detect your reader, read on:</p>
<blockquote> <blockquote>
<p> <p>

View File

@ -108,8 +108,7 @@ sudo python -c "import urllib2; exec urllib2.urlopen('http://status.calibre-eboo
<pre class="wiki"> <pre class="wiki">
wget -O- http://calibre.kovidgoyal.net/downloads/${app}-${version}.tar.gz | tar xvz wget -O- http://calibre.kovidgoyal.net/downloads/${app}-${version}.tar.gz | tar xvz
cd calibre* cd calibre*
python setup.py build_ext &amp;&amp; python setup.py build &amp;&amp; sudo python setup.py install sudo python setup.py install
sudo calibre_postinstall
</pre> </pre>
Note that if your distribution does not have a Note that if your distribution does not have a
correctly compiled libunrar.so, ${app} will not correctly compiled libunrar.so, ${app} will not

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -20,6 +20,8 @@ from collections import defaultdict
if os.environ.has_key('CALIBRE_CONFIG_DIRECTORY'): if os.environ.has_key('CALIBRE_CONFIG_DIRECTORY'):
config_dir = os.path.abspath(os.environ['CALIBRE_CONFIG_DIRECTORY']) config_dir = os.path.abspath(os.environ['CALIBRE_CONFIG_DIRECTORY'])
elif iswindows: elif iswindows:
if plugins['winutil'][0] is None:
raise Exception(plugins['winutil'][1])
config_dir = plugins['winutil'][0].special_folder_path(plugins['winutil'][0].CSIDL_APPDATA) config_dir = plugins['winutil'][0].special_folder_path(plugins['winutil'][0].CSIDL_APPDATA)
if not os.access(config_dir, os.W_OK|os.X_OK): if not os.access(config_dir, os.W_OK|os.X_OK):
config_dir = os.path.expanduser('~') config_dir = os.path.expanduser('~')

View File

@ -6,5 +6,30 @@ __license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import os
from threading import Thread
from calibre.constants import iswindows
ADDRESS = r'\\.\pipe\CalibreGUI' if iswindows else \
os.path.expanduser('~/.calibre-gui.socket')
class RC(Thread):
def __init__(self, print_error=True):
self.print_error = print_error
Thread.__init__(self)
self.conn = None
def run(self):
from multiprocessing.connection import Client
self.done = False
try:
self.conn = Client(ADDRESS)
self.done = True
except:
if self.print_error:
import traceback
traceback.print_exc()

View File

@ -30,6 +30,7 @@ class BaseJob(object):
self.done2 = None self.done2 = None
self.killed = False self.killed = False
self.failed = False self.failed = False
self.kill_on_start = False
self.start_time = None self.start_time = None
self.result = None self.result = None
self.duration = None self.duration = None

View File

@ -6,7 +6,7 @@ __license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import os, cPickle, time, tempfile import sys, os, cPickle, time, tempfile
from math import ceil from math import ceil
from threading import Thread, RLock from threading import Thread, RLock
from Queue import Queue, Empty from Queue import Queue, Empty
@ -83,14 +83,16 @@ class CriticalError(Exception):
class Server(Thread): class Server(Thread):
def __init__(self, notify_on_job_done=lambda x: x, pool_size=None): def __init__(self, notify_on_job_done=lambda x: x, pool_size=None,
limit=sys.maxint):
Thread.__init__(self) Thread.__init__(self)
self.daemon = True self.daemon = True
global _counter global _counter
self.id = _counter+1 self.id = _counter+1
_counter += 1 _counter += 1
self.pool_size = cpu_count() if pool_size is None else pool_size limit = min(limit, cpu_count())
self.pool_size = limit if pool_size is None else pool_size
self.notify_on_job_done = notify_on_job_done self.notify_on_job_done = notify_on_job_done
self.auth_key = os.urandom(32) self.auth_key = os.urandom(32)
self.address = arbitrary_address('AF_PIPE' if iswindows else 'AF_UNIX') self.address = arbitrary_address('AF_PIPE' if iswindows else 'AF_UNIX')
@ -168,6 +170,7 @@ class Server(Thread):
except Empty: except Empty:
pass pass
# Get notifications from worker process
for worker in self.workers: for worker in self.workers:
while True: while True:
try: try:
@ -177,6 +180,7 @@ class Server(Thread):
except Empty: except Empty:
break break
# Remove finished jobs
for worker in [w for w in self.workers if not w.is_alive]: for worker in [w for w in self.workers if not w.is_alive]:
self.workers.remove(worker) self.workers.remove(worker)
job = worker.job job = worker.job
@ -189,19 +193,27 @@ class Server(Thread):
job.duration = time.time() - job.start_time job.duration = time.time() - job.start_time
self.changed_jobs_queue.put(job) self.changed_jobs_queue.put(job)
# Start new workers
if len(self.pool) + len(self.workers) < self.pool_size: if len(self.pool) + len(self.workers) < self.pool_size:
try: try:
self.pool.append(self.launch_worker()) self.pool.append(self.launch_worker())
except Exception: except Exception:
pass pass
# Start waiting jobs
if len(self.pool) > 0 and len(self.waiting_jobs) > 0: if len(self.pool) > 0 and len(self.waiting_jobs) > 0:
job = self.waiting_jobs.pop() job = self.waiting_jobs.pop()
worker = self.pool.pop()
job.start_time = time.time() job.start_time = time.time()
worker.start_job(job) if job.kill_on_start:
self.workers.append(worker) job.duration = 0.0
job.log_path = worker.log_path job.returncode = 1
job.killed = job.failed = True
job.result = None
else:
worker = self.pool.pop()
worker.start_job(job)
self.workers.append(worker)
job.log_path = worker.log_path
self.changed_jobs_queue.put(job) self.changed_jobs_queue.put(job)
while True: while True:
@ -215,11 +227,13 @@ class Server(Thread):
self.kill_queue.put(job) self.kill_queue.put(job)
def killall(self): def killall(self):
for job in self.workers: for worker in self.workers:
self.kill_queue.put(job) self.kill_queue.put(worker.job)
def _kill_job(self, job): def _kill_job(self, job):
if job.start_time is None: return if job.start_time is None:
job.kill_on_start = True
return
for worker in self.workers: for worker in self.workers:
if job is worker.job: if job is worker.job:
worker.kill() worker.kill()

View File

@ -57,7 +57,7 @@ recipe_modules = ['recipe_' + r for r in (
'monitor', 'republika', 'beta', 'beta_en', 'glasjavnosti', 'monitor', 'republika', 'beta', 'beta_en', 'glasjavnosti',
'esquire', 'livemint', 'thedgesingapore', 'darknet', 'rga', 'esquire', 'livemint', 'thedgesingapore', 'darknet', 'rga',
'intelligencer', 'theoldfoodie', 'hln_be', 'honvedelem', 'intelligencer', 'theoldfoodie', 'hln_be', 'honvedelem',
'the_new_republic', 'the_new_republic', 'philly',
)] )]

View File

@ -7,7 +7,7 @@ lemonde.fr
''' '''
import re import re
from datetime import date #from datetime import date
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
@ -20,7 +20,28 @@ class LeMonde(BasicNewsRecipe):
max_articles_per_feed = 30 max_articles_per_feed = 30
no_stylesheets = True no_stylesheets = True
cover_url='http://abonnes.lemonde.fr/titresdumonde/'+date.today().strftime("%y%m%d")+'/1.jpg' remove_javascript = True
#cover_url='http://abonnes.lemonde.fr/titresdumonde/'+date.today().strftime("%y%m%d")+'/1.jpg'
extra_css = '''
.dateline{color:#666666;font-family:verdana,sans-serif;font-size:xx-small;}
.mainText{font-family:Georgia,serif;color:#222222;}
.LM_articleText{font-family:Georgia,serif;}
.mainContent{font-family:Georgia,serif;}
.mainTitle{font-family:Georgia,serif;}
.LM_content{font-family:Georgia,serif;}
.content{font-family:Georgia,serif;}
.LM_caption{font-family:Georgia,serif;font-size:xx-small;}
.LM_imageSource{font-family:Arial,Helvetica,sans-serif;font-size:xx-small;color:#666666;}
h1{font-family:Georgia,serif;font-size:medium;color:#000000;}
.entry{font-family:Georgia,Times New Roman,Times,serif;}
.mainTitle{font-family:Georgia,Times New Roman,Times,serif;}
h2{font-family:Georgia,Times New Roman,Times,serif;font-size:large;}
small{{font-family:Arial,Helvetica,sans-serif;font-size:xx-small;}
'''
feeds = [ feeds = [
('A la Une', 'http://www.lemonde.fr/rss/une.xml'), ('A la Une', 'http://www.lemonde.fr/rss/une.xml'),
@ -40,14 +61,20 @@ class LeMonde(BasicNewsRecipe):
('Examens', 'http://www.lemonde.fr/rss/sequence/0,2-3404,1-0,0.xml'), ('Examens', 'http://www.lemonde.fr/rss/sequence/0,2-3404,1-0,0.xml'),
('Opinions', 'http://www.lemonde.fr/rss/sequence/0,2-3232,1-0,0.xml') ('Opinions', 'http://www.lemonde.fr/rss/sequence/0,2-3232,1-0,0.xml')
] ]
keep_only_tags = [dict(name='div', attrs={'id':["mainTitle","mainContent","LM_content","content"]}),
dict(name='div', attrs={'class':["post"]})
]
remove_tags = [dict(name='img', attrs={'src':'http://medias.lemonde.fr/mmpub/img/lgo/lemondefr_pet.gif'}), remove_tags = [dict(name='img', attrs={'src':'http://medias.lemonde.fr/mmpub/img/lgo/lemondefr_pet.gif'}),
dict(name='div', attrs={'id':'xiti-logo-noscript'}), dict(name='div', attrs={'id':'xiti-logo-noscript'}),
dict(name='br', attrs={}), dict(name='br', attrs={}),
dict(name='iframe', attrs={}), dict(name='iframe', attrs={}),
dict(name='table', attrs={'id':["toolBox"]}),
dict(name='table', attrs={'class':["bottomToolBox"]}),
dict(name='div', attrs={'class':["pageNavigation","fenetreBoxesContainer","breakingNews","LM_toolsBottom","LM_comments","LM_tools","pave_meme_sujet_hidden","boxMemeSujet"]}),
dict(name='div', attrs={'id':["miniUne","LM_sideBar"]}),
] ]
extra_css = '.ar-tit {font-size: x-large;} \n .dt {font-size: x-small;}'
preprocess_regexps = [ (re.compile(i[0], re.IGNORECASE|re.DOTALL), i[1]) for i in preprocess_regexps = [ (re.compile(i[0], re.IGNORECASE|re.DOTALL), i[1]) for i in
[ [
@ -68,8 +95,8 @@ class LeMonde(BasicNewsRecipe):
] ]
] ]
def print_version(self, url): # def print_version(self, url):
return re.sub('http://www\.lemonde\.fr/.*_([0-9]+)_[0-9]+\.html.*','http://www.lemonde.fr/web/imprimer_element/0,40-0,50-\\1,0.html' ,url) # return re.sub('http://www\.lemonde\.fr/.*_([0-9]+)_[0-9]+\.html.*','http://www.lemonde.fr/web/imprimer_element/0,40-0,50-\\1,0.html' ,url)
# Used to filter duplicated articles # Used to filter duplicated articles
articles_list = [] articles_list = []
@ -94,3 +121,5 @@ class LeMonde(BasicNewsRecipe):
return True return True
return False return False
return False return False

View File

@ -0,0 +1,56 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
'''
philly.com/inquirer/
'''
import re
from calibre.web.feeds.recipes import BasicNewsRecipe
class Philly(BasicNewsRecipe):
title = 'Philadelphia Inquirer'
__author__ = 'RadikalDissent'
language = 'en'
description = 'Daily news from the Philadelphia Inquirer'
no_stylesheets = True
use_embedded_content = False
oldest_article = 1
max_articles_per_feed = 25
extra_css = '''
.byline {font-size: small; color: grey; font-style:italic; }
.lastline {font-size: small; color: grey; font-style:italic;}
.contact {font-size: small; color: grey;}
.contact p {font-size: small; color: grey;}
'''
preprocess_regexps = [(re.compile(i[0], re.IGNORECASE | re.DOTALL), i[1]) for i in
[
(r'<body.*<h1>', lambda match: '<body><h1>'),
(r'<font size="2" face="Arial">', lambda match: '<div class="contact"><font class="contact">'),
(r'<font face="Arial" size="2">', lambda match: '<div class="contact"><font class="contact">')
]
]
keep_only_tags = [
dict(name='h1'),
dict(name='p', attrs={'class':['byline','lastline']}),
dict(name='div', attrs={'class':'body-content'}),
]
remove_tags = [
dict(name='hr'),
dict(name='p', attrs={'class':'buzzBadge'}),
]
def print_version(self, url):
return url + '?viewAll=y'
feeds = [
('Front Page', 'http://www.philly.com/inquirer_front_page.rss'),
('Business', 'http://www.philly.com/inq_business.rss'),
('News', 'http://www.philly.com/inquirer/news/index.rss'),
('Nation', 'http://www.philly.com/inq_news_world_us.rss'),
('Local', 'http://www.philly.com/inquirer_local.rss'),
('Health', 'http://www.philly.com/inquirer_health_science.rss'),
('Education', 'http://www.philly.com/inquirer_education.rss'),
('Editorial and opinion', 'http://www.philly.com/inq_news_editorial.rss'),
('Sports', 'http://www.philly.com/inquirer_sports.rss')
]