diff --git a/setup.py b/setup.py
index a0bfbcaa0e..5111d16c45 100644
--- a/setup.py
+++ b/setup.py
@@ -380,13 +380,16 @@ if __name__ == '__main__':
class build(_build):
- sub_commands = \
- [
+ sub_commands = [
('resources', lambda self : 'CALIBRE_BUILDBOT' not in os.environ.keys()),
('translations', lambda self : 'CALIBRE_BUILDBOT' not in os.environ.keys()),
('gui', lambda self : 'CALIBRE_BUILDBOT' not in os.environ.keys()),
- ] + _build.sub_commands
-
+ ('build_ext', lambda self: True),
+ ('build_py', lambda self: True),
+ ('build_clib', _build.has_c_libraries),
+ ('build_scripts', _build.has_scripts),
+ ]
+
entry_points['console_scripts'].append('calibre_postinstall = calibre.linux:post_install')
ext_modules = [
Extension('calibre.plugins.lzx',
diff --git a/src/calibre/__init__.py b/src/calibre/__init__.py
index f483902126..7d44bef8f7 100644
--- a/src/calibre/__init__.py
+++ b/src/calibre/__init__.py
@@ -13,7 +13,8 @@ from calibre.startup import plugins, winutil, winutilerror
from calibre.constants import iswindows, isosx, islinux, isfrozen, \
terminal_controller, preferred_encoding, \
__appname__, __version__, __author__, \
- win32event, win32api, winerror, fcntl
+ win32event, win32api, winerror, fcntl, \
+ filesystem_encoding
import mechanize
mimetypes.add_type('application/epub+zip', '.epub')
@@ -41,6 +42,25 @@ def osx_version():
return int(m.group(1)), int(m.group(2)), int(m.group(3))
+_filename_sanitize = re.compile(r'[\xae\0\\|\?\*<":>\+\[\]/]')
+
+def sanitize_file_name(name, substitute='_'):
+ '''
+ Sanitize the filename `name`. All invalid characters are replaced by `substitute`.
+ The set of invalid characters is the union of the invalid characters in Windows,
+ OS X and Linux. Also removes leading an trailing whitespace.
+ **WARNING:** This function also replaces path separators, so only pass file names
+ and not full paths to it.
+ *NOTE:* This function always returns byte strings, not unicode objects. The byte strings
+ are encoded in the filesystem encoding of the platform, or UTF-8.
+ '''
+ if isinstance(name, unicode):
+ name = name.encode(filesystem_encoding, 'ignore')
+ one = _filename_sanitize.sub(substitute, name)
+ one = re.sub(r'\s', ' ', one).strip()
+ return re.sub(r'^\.+$', '_', one)
+
+
class CommandLineError(Exception):
pass
@@ -201,13 +221,6 @@ class CurrentDir(object):
def __exit__(self, *args):
os.chdir(self.cwd)
-def sanitize_file_name(name):
- '''
- Remove characters that are illegal in filenames from name.
- Also remove path separators. All illegal characters are replaced by
- underscores.
- '''
- return re.sub(r'\s', ' ', re.sub(r'[\xae"\'\|\~\:\?\\\/]|^-', '_', name.strip()))
def detect_ncpus():
"""Detects the number of effective CPUs in the system"""
diff --git a/src/calibre/constants.py b/src/calibre/constants.py
index bfbebd5273..7454c84dd9 100644
--- a/src/calibre/constants.py
+++ b/src/calibre/constants.py
@@ -29,6 +29,10 @@ winerror = __import__('winerror') if iswindows else None
win32api = __import__('win32api') if iswindows else None
fcntl = None if iswindows else __import__('fcntl')
+filesystem_encoding = sys.getfilesystemencoding()
+if filesystem_encoding is None: filesystem_encoding = 'utf-8'
+
+
################################################################################
plugins = None
if plugins is None:
diff --git a/src/calibre/customize/ui.py b/src/calibre/customize/ui.py
index 68e17fd0aa..240621c51e 100644
--- a/src/calibre/customize/ui.py
+++ b/src/calibre/customize/ui.py
@@ -185,6 +185,20 @@ def add_plugin(path_to_zip_file):
initialize_plugins()
return plugin
+def remove_plugin(plugin_or_name):
+ name = getattr(plugin_or_name, 'name', plugin_or_name)
+ plugins = config['plugins']
+ removed = False
+ if name in plugins.keys():
+ removed = True
+ zfp = plugins[name]
+ if os.path.exists(zfp):
+ os.remove(zfp)
+ plugins.pop(name)
+ config['plugins'] = plugins
+ initialize_plugins()
+ return removed
+
def is_disabled(plugin):
return plugin.name in config['disabled_plugins']
@@ -237,6 +251,8 @@ def option_parser():
'''))
parser.add_option('-a', '--add-plugin', default=None,
help=_('Add a plugin by specifying the path to the zip file containing it.'))
+ parser.add_option('-r', '--remove-plugin', default=None,
+ help=_('Remove a custom plugin by name. Has no effect on builtin plugins'))
parser.add_option('--customize-plugin', default=None,
help=_('Customize plugin. Specify name of plugin and customization string separated by a comma.'))
parser.add_option('-l', '--list-plugins', default=False, action='store_true',
@@ -267,6 +283,11 @@ def main(args=sys.argv):
if opts.add_plugin is not None:
plugin = add_plugin(opts.add_plugin)
print 'Plugin added:', plugin.name, plugin.version
+ if opts.remove_plugin is not None:
+ if remove_plugin(opts.remove_plugin):
+ print 'Plugin removed'
+ else:
+ print 'No custom pluginnamed', opts.remove_plugin
if opts.customize_plugin is not None:
name, custom = opts.customize_plugin.split(',')
plugin = find_plugin(name.strip())
diff --git a/src/calibre/ebooks/lrf/html/convert_from.py b/src/calibre/ebooks/lrf/html/convert_from.py
index 2ed8d29468..fb511f0499 100644
--- a/src/calibre/ebooks/lrf/html/convert_from.py
+++ b/src/calibre/ebooks/lrf/html/convert_from.py
@@ -1906,6 +1906,8 @@ def process_file(path, options, logger=None):
fpb = re.compile(options.force_page_break, re.IGNORECASE) if options.force_page_break else \
re.compile('$')
cq = options.chapter_attr.split(',')
+ if len(cq) < 3:
+ raise ValueError('The --chapter-attr setting must have 2 commas.')
options.chapter_attr = [re.compile(cq[0], re.IGNORECASE), cq[1],
re.compile(cq[2], re.IGNORECASE)]
options.force_page_break = fpb
diff --git a/src/calibre/ebooks/mobi/reader.py b/src/calibre/ebooks/mobi/reader.py
index 420159299e..5e553dcdd5 100644
--- a/src/calibre/ebooks/mobi/reader.py
+++ b/src/calibre/ebooks/mobi/reader.py
@@ -22,6 +22,7 @@ from calibre.ebooks.mobi.langcodes import main_language, sub_language
from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks.metadata.opf import OPFCreator
from calibre.ebooks.metadata.toc import TOC
+from calibre import sanitize_file_name
class EXTHHeader(object):
@@ -200,7 +201,8 @@ class MobiReader(object):
guide = soup.find('guide')
for elem in soup.findAll(['metadata', 'guide']):
elem.extract()
- htmlfile = os.path.join(output_dir, self.name+'.html')
+ htmlfile = os.path.join(output_dir,
+ sanitize_file_name(self.name)+'.html')
try:
for ref in guide.findAll('reference', href=True):
ref['href'] = os.path.basename(htmlfile)+ref['href']
diff --git a/src/calibre/gui2/dialogs/config.py b/src/calibre/gui2/dialogs/config.py
index 82b07e81e7..def355d974 100644
--- a/src/calibre/gui2/dialogs/config.py
+++ b/src/calibre/gui2/dialogs/config.py
@@ -20,7 +20,7 @@ from calibre.ebooks.epub.iterator import is_supported
from calibre.library import server_config
from calibre.customize.ui import initialized_plugins, is_disabled, enable_plugin, \
disable_plugin, customize_plugin, \
- plugin_customization, add_plugin
+ plugin_customization, add_plugin, remove_plugin
class PluginModel(QAbstractItemModel):
@@ -242,6 +242,7 @@ class ConfigDialog(QDialog, Ui_Dialog):
self.plugin_view.setModel(self._plugin_model)
self.connect(self.toggle_plugin, SIGNAL('clicked()'), lambda : self.modify_plugin(op='toggle'))
self.connect(self.customize_plugin, SIGNAL('clicked()'), lambda : self.modify_plugin(op='customize'))
+ self.connect(self.remove_plugin, SIGNAL('clicked()'), lambda : self.modify_plugin(op='remove'))
self.connect(self.button_plugin_browse, SIGNAL('clicked()'), self.find_plugin)
self.connect(self.button_plugin_add, SIGNAL('clicked()'), self.add_plugin)
@@ -287,6 +288,13 @@ class ConfigDialog(QDialog, Ui_Dialog):
if ok:
customize_plugin(plugin, unicode(text))
self._plugin_model.refresh_plugin(plugin)
+ if op == 'remove':
+ if remove_plugin(plugin):
+ self._plugin_model.populate()
+ self._plugin_model.reset()
+ else:
+ error_dialog(self, _('Cannot remove builtin plugin'),
+ plugin.name + _(' cannot be removed. It is a builtin plugin. Try disabling it instead.')).exec_()
def up_column(self):
diff --git a/src/calibre/gui2/dialogs/config.ui b/src/calibre/gui2/dialogs/config.ui
index 05b66cac88..99c74fc345 100644
--- a/src/calibre/gui2/dialogs/config.ui
+++ b/src/calibre/gui2/dialogs/config.ui
@@ -72,7 +72,7 @@
- 0
+ 4
@@ -811,6 +811,13 @@
+ -
+
+
+ &Remove plugin
+
+
+
-
diff --git a/src/calibre/gui2/main.py b/src/calibre/gui2/main.py
index 4bbde629e5..abf0aed0fa 100644
--- a/src/calibre/gui2/main.py
+++ b/src/calibre/gui2/main.py
@@ -187,8 +187,8 @@ class Main(MainWindow, Ui_MainWindow):
self.metadata_menu = md
self.add_menu = QMenu()
self.add_menu.addAction(_('Add books from a single directory'))
- self.add_menu.addAction(_('Add books recursively (One book per directory, assumes every ebook file is the same book in a different format)'))
- self.add_menu.addAction(_('Add books recursively (Multiple books per directory, assumes every ebook file is a different book)'))
+ self.add_menu.addAction(_('Add books from directories, including sub-directories (One book per directory, assumes every ebook file is the same book in a different format)'))
+ self.add_menu.addAction(_('Add books from directories, including sub directories (Multiple books per directory, assumes every ebook file is a different book)'))
self.action_add.setMenu(self.add_menu)
QObject.connect(self.action_add, SIGNAL("triggered(bool)"), self.add_books)
QObject.connect(self.add_menu.actions()[0], SIGNAL("triggered(bool)"), self.add_books)
diff --git a/src/calibre/gui2/status.py b/src/calibre/gui2/status.py
index 31fd0c9112..75a967943e 100644
--- a/src/calibre/gui2/status.py
+++ b/src/calibre/gui2/status.py
@@ -199,7 +199,10 @@ class StatusBar(QStatusBar):
ret = QStatusBar.showMessage(self, msg, timeout)
if self.systray is not None:
if isosx and isinstance(msg, unicode):
- msg = msg.encode(preferred_encoding)
+ try:
+ msg = msg.encode(preferred_encoding)
+ except UnicodeEncodeError:
+ msg = msg.encode('utf-8')
self.systray.showMessage('calibre', msg, self.systray.Information, 10000)
return ret
diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py
index 244ae72aeb..161442e840 100644
--- a/src/calibre/library/database2.py
+++ b/src/calibre/library/database2.py
@@ -21,13 +21,12 @@ from calibre.library.sqlite import connect, IntegrityError
from calibre.utils.search_query_parser import SearchQueryParser
from calibre.ebooks.metadata import string_to_authors, authors_to_string
from calibre.ebooks.metadata.meta import get_metadata
-from calibre.constants import preferred_encoding, iswindows, isosx
+from calibre.constants import preferred_encoding, iswindows, isosx, filesystem_encoding
from calibre.ptempfile import PersistentTemporaryFile
from calibre.customize.ui import run_plugins_on_import
+from calibre import sanitize_file_name
copyfile = os.link if hasattr(os, 'link') else shutil.copyfile
-filesystem_encoding = sys.getfilesystemencoding()
-if filesystem_encoding is None: filesystem_encoding = 'utf-8'
iscaseinsensitive = iswindows or isosx
def normpath(x):
@@ -37,23 +36,6 @@ def normpath(x):
x = x.lower()
return x
-_filename_sanitize = re.compile(r'[\xae\0\\|\?\*<":>\+\[\]/]')
-
-def sanitize_file_name(name, substitute='_'):
- '''
- Sanitize the filename `name`. All invalid characters are replaced by `substitute`.
- The set of invalid characters is the union of the invalid characters in Windows,
- OS X and Linux. Also removes leading an trailing whitespace.
- **WARNING:** This function also replaces path separators, so only pass file names
- and not full paths to it.
- *NOTE:* This function always returns byte strings, not unicode objects. The byte strings
- are encoded in the filesystem encoding of the platform, or UTF-8.
- '''
- if isinstance(name, unicode):
- name = name.encode(filesystem_encoding, 'ignore')
- one = _filename_sanitize.sub(substitute, name)
- one = re.sub(r'\s', ' ', one).strip()
- return re.sub(r'^\.+$', '_', one)
FIELD_MAP = {'id':0, 'title':1, 'authors':2, 'publisher':3, 'rating':4, 'timestamp':5,
'size':6, 'tags':7, 'comments':8, 'series':9, 'series_index':10,
diff --git a/src/calibre/trac/donations/server.py b/src/calibre/trac/donations/server.py
index 2472524e95..18771d6fdf 100644
--- a/src/calibre/trac/donations/server.py
+++ b/src/calibre/trac/donations/server.py
@@ -199,7 +199,7 @@ class Server(object):
x = list(range(days-1, -1, -1))
y = stats.daily_totals
ax.plot(x, y)#, align='center', width=20, color='g')
- ax.set_xlabel('Day')
+ ax.set_xlabel('Days ago')
ax.set_ylabel('Income ($)')
ax.hlines([stats.daily_average], 0, days-1)
ax.set_xlim([0, days-1])