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