From 2c6abe6a44602f864ee9da66b3fdfd2b76e0a465 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 26 Mar 2011 17:13:27 -0600 Subject: [PATCH 01/37] First stab at a PEP 302 based plugin importer --- src/calibre/customize/zipplugin.py | 86 ++++++++++++++++++++++-------- 1 file changed, 64 insertions(+), 22 deletions(-) diff --git a/src/calibre/customize/zipplugin.py b/src/calibre/customize/zipplugin.py index 194f444207..0df64ce79c 100644 --- a/src/calibre/customize/zipplugin.py +++ b/src/calibre/customize/zipplugin.py @@ -2,12 +2,13 @@ # vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai from __future__ import (unicode_literals, division, absolute_import, print_function) +from future_builtins import map __license__ = 'GPL v3' __copyright__ = '2011, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import os, zipfile, posixpath, importlib, threading, re +import os, zipfile, posixpath, importlib, threading, re, imp, sys from collections import OrderedDict from calibre.customize import (Plugin, numeric_version, platform, @@ -20,28 +21,67 @@ from calibre.customize import (Plugin, numeric_version, platform, class PluginLoader(object): - ''' - The restrictions that a zip file must obey to be a valid calibre plugin - are: - - * The .py file that defines the main plugin class must have a name - that: - * Ends in plugin.py - * Is a valid python identifier (contains only English alphabets, - underscores and numbers and starts with an alphabet). This - applies to the file name minus the .py extension, obviously. - * Try to make this name as distinct as possible, as it will be - put into a global namespace of all plugins. - * The zip file must contain a .py file that defines the main plugin - class at the top level. That is, it must not be in a subdirectory. - The filename must follow the restrictions outlined above. - ''' - def __init__(self): self.loaded_plugins = {} self._lock = threading.RLock() self._identifier_pat = re.compile(r'[a-zA-Z][_0-9a-zA-Z]*') + def _get_actual_fullname(self, fullname): + parts = fullname.split('.') + if parts[0] == 'calibre_plugins': + if len(parts) == 1: + return parts[0], None + plugin_name = parts[1] + with self._lock: + names = self.loaded_plugins.get(plugin_name, None)[1] + if names is None: + raise ImportError('No plugin named %r loaded'%plugin_name) + fullname = '.'.join(parts[2:]) + if not fullname: + fullname = '__init__' + if fullname in names: + return fullname, plugin_name + if fullname+'.__init__' in names: + return fullname+'.__init__', plugin_name + return None, None + + def find_module(self, fullname, path=None): + fullname, plugin_name = self._get_actual_fullname(fullname) + if fullname is None and plugin_name is None: + return None + return self + + def load_module(self, fullname): + import_name, plugin_name = self._get_actual_fullname(fullname) + if import_name is None and plugin_name is None: + raise ImportError('No plugin named %r is loaded'%fullname) + mod = sys.modules.setdefault(fullname, imp.new_module(fullname)) + mod.__file__ = "" + mod.__loader__ = self + + if import_name.endswith('.__init__') or import_name in ('__init__', + 'calibre_plugins'): + # We have a package + mod.__path__ = [] + + if plugin_name is not None: + # We have some actual code to load + with self._lock: + zfp, names = self.loaded_plugins.get(plugin_name, (None, None)) + if names is None: + raise ImportError('No plugin named %r loaded'%plugin_name) + zinfo = names.get(import_name, None) + if zinfo is None: + raise ImportError('Plugin %r has no module named %r' % + (plugin_name, import_name)) + with zipfile.ZipFile(zfp) as zf: + code = zf.read(zinfo) + compiled = compile(code, 'import_name', 'exec', dont_inherit=True) + exec compiled in mod.__dict__ + + return mod + + def load(self, path_to_zip_file): if not os.access(path_to_zip_file, os.R_OK): raise PluginNotFound('Cannot access %r'%path_to_zip_file) @@ -52,7 +92,7 @@ class PluginLoader(object): try: ans = None m = importlib.import_module( - 'calibre_plugins.%s.__init__'%plugin_name) + 'calibre_plugins.%s'%plugin_name) for obj in m.__dict__.itervalues(): if isinstance(obj, type) and issubclass(obj, Plugin) and \ obj.name != 'Trivial Plugin': @@ -62,10 +102,11 @@ class PluginLoader(object): raise InvalidPlugin('No plugin class found in %r:%r'%( path_to_zip_file, plugin_name)) - if ans.minimum_calibre_version < numeric_version: + if ans.minimum_calibre_version > numeric_version: raise InvalidPlugin( 'The plugin at %r needs a version of calibre >= %r' % - (path_to_zip_file, '.'.join(ans.minimum_calibre_version))) + (path_to_zip_file, '.'.join(map(str, + ans.minimum_calibre_version)))) if platform not in ans.supported_platforms: raise InvalidPlugin( @@ -123,7 +164,7 @@ class PluginLoader(object): names = OrderedDict() - for candidate in names: + for candidate in pynames: parts = posixpath.splitext(candidate)[0].split('/') package = '.'.join(parts[:-1]) if package and package not in valid_packages: @@ -150,5 +191,6 @@ class PluginLoader(object): loader = PluginLoader() +sys.meta_path.insert(0, loader) From a18b3ad33dc8ddbfc9707e88a34ad2ca352749c6 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 26 Mar 2011 19:25:05 -0600 Subject: [PATCH 02/37] Cracked.com by Nudgenudge --- recipes/cracked_com.recipe | 83 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 recipes/cracked_com.recipe diff --git a/recipes/cracked_com.recipe b/recipes/cracked_com.recipe new file mode 100644 index 0000000000..49ed9d2279 --- /dev/null +++ b/recipes/cracked_com.recipe @@ -0,0 +1,83 @@ +from calibre.web.feeds.news import BasicNewsRecipe +import re + +class Cracked(BasicNewsRecipe): + title = u'Cracked.com' + __author__ = u'Nudgenudge' + language = 'en' + description = 'America''s Only Humor and Video Site, since 1958' + publisher = 'Cracked' + category = 'comedy, lists' + oldest_article = 2 + delay = 10 + max_articles_per_feed = 2 + no_stylesheets = True + encoding = 'cp1252' + remove_javascript = True + use_embedded_content = False + INDEX = u'http://www.cracked.com' + extra_css = """ + .pageheader_type{font-size: x-large; font-weight: bold; color: #828D74} + .pageheader_title{font-size: xx-large; color: #394128} + .pageheader_byline{font-size: small; font-weight: bold; color: #394128} + .score_bg {display: inline; width: 100%; margin-bottom: 2em} + .score_column_1{ padding-left: 10px; font-size: small; width: 50%} + .score_column_2{ padding-left: 10px; font-size: small; width: 50%} + .score_column_3{ padding-left: 10px; font-size: small; width: 50%} + .score_header{font-size: large; color: #50544A} + .bodytext{display: block} + body{font-family: Helvetica,Arial,sans-serif} + """ + + conversion_options = { + 'comment' : description + , 'tags' : category + , 'publisher' : publisher + , 'language' : language + , 'linearize_tables' : True + } + + keep_only_tags = [ + dict(name='div', attrs={'class':['Column1']}) + ] + + feeds = [(u'Articles', u'http://feeds.feedburner.com/CrackedRSS')] + + def get_article_url(self, article): + return article.get('guid', None) + + def cleanup_page(self, soup): + for item in soup.findAll(style=True): + del item['style'] + for alink in soup.findAll('a'): + if alink.string is not None: + tstr = alink.string + alink.replaceWith(tstr) + for div_to_remove in soup.findAll('div', attrs={'id':['googlead_1','fb-like-article','comments_section']}): + div_to_remove.extract() + for div_to_remove in soup.findAll('div', attrs={'class':['share_buttons_col_1','GenericModule1']}): + div_to_remove.extract() + for div_to_remove in soup.findAll('div', attrs={'class':re.compile("prev_next")}): + div_to_remove.extract() + for ul_to_remove in soup.findAll('ul', attrs={'class':['Nav6']}): + ul_to_remove.extract() + for image in soup.findAll('img', attrs={'alt': 'article image'}): + image.extract() + + def append_page(self, soup, appendtag, position): + pager = soup.find('a',attrs={'class':'next_arrow_active'}) + if pager: + nexturl = self.INDEX + pager['href'] + soup2 = self.index_to_soup(nexturl) + texttag = soup2.find('div', attrs={'class':re.compile("userStyled")}) + newpos = len(texttag.contents) + self.append_page(soup2,texttag,newpos) + texttag.extract() + self.cleanup_page(appendtag) + appendtag.insert(position,texttag) + else: + self.cleanup_page(appendtag) + + def preprocess_html(self, soup): + self.append_page(soup, soup.body, 3) + return self.adeify_images(soup) From ae1c331d3c3747782df535853a103a18d6886da8 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 27 Mar 2011 07:53:17 -0600 Subject: [PATCH 03/37] Start work on an example plugin demonstrating the new plugin loader --- setup/upload.py | 19 ++- src/calibre/customize/zipplugin.py | 84 +++++++++- src/calibre/gui2/ui.py | 3 + src/calibre/manual/creating_plugins.rst | 60 +++++++ src/calibre/manual/customize.rst | 151 ++---------------- .../plugin_examples/helloworld/__init__.py | 33 ++++ .../plugin_examples/interface/__init__.py | 33 ++++ .../plugin_examples/interface/images/icon.png | Bin 0 -> 5785 bytes .../plugin-import-name-interface.txt | 0 .../manual/plugin_examples/interface/ui.py | 30 ++++ src/calibre/manual/tutorials.rst | 1 + 11 files changed, 265 insertions(+), 149 deletions(-) create mode 100644 src/calibre/manual/creating_plugins.rst create mode 100644 src/calibre/manual/plugin_examples/helloworld/__init__.py create mode 100644 src/calibre/manual/plugin_examples/interface/__init__.py create mode 100644 src/calibre/manual/plugin_examples/interface/images/icon.png create mode 100644 src/calibre/manual/plugin_examples/interface/plugin-import-name-interface.txt create mode 100644 src/calibre/manual/plugin_examples/interface/ui.py diff --git a/setup/upload.py b/setup/upload.py index 3bad1dd8f3..468c0a61b3 100644 --- a/setup/upload.py +++ b/setup/upload.py @@ -1,14 +1,14 @@ #!/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 ' __docformat__ = 'restructuredtext en' -import os, re, cStringIO, base64, httplib, subprocess, hashlib, shutil, time +import os, re, cStringIO, base64, httplib, subprocess, hashlib, shutil, time, glob from subprocess import check_call from tempfile import NamedTemporaryFile, mkdtemp +from zipfile import ZipFile from setup import Command, __version__, installer_name, __appname__ @@ -341,7 +341,22 @@ class UploadUserManual(Command): # {{{ description = 'Build and upload the User Manual' sub_commands = ['manual'] + def build_plugin_example(self, path): + from calibre import CurrentDir + with NamedTemporaryFile(suffix='.zip') as f: + with CurrentDir(self.d(path)): + with ZipFile(f, 'w') as zf: + for x in os.listdir('.'): + zf.write(x) + bname = self.b(path) + '_plugin.zip' + subprocess.check_call(['scp', f.name, 'divok:%s/%s'%(DOWNLOADS, + bname)]) + def run(self, opts): + path = self.j(self.SRC, 'calibre', 'manual', 'plugin_examples') + for x in glob.glob(self.j(path, '*')): + self.build_plugin_example(x) + check_call(' '.join(['scp', '-r', 'src/calibre/manual/.build/html/*', 'divok:%s'%USER_MANUAL]), shell=True) # }}} diff --git a/src/calibre/customize/zipplugin.py b/src/calibre/customize/zipplugin.py index 0df64ce79c..932337f624 100644 --- a/src/calibre/customize/zipplugin.py +++ b/src/calibre/customize/zipplugin.py @@ -10,6 +10,7 @@ __docformat__ = 'restructuredtext en' import os, zipfile, posixpath, importlib, threading, re, imp, sys from collections import OrderedDict +from functools import partial from calibre.customize import (Plugin, numeric_version, platform, InvalidPlugin, PluginNotFound) @@ -18,6 +19,65 @@ from calibre.customize import (Plugin, numeric_version, platform, # python 2.x that prevents importing from zip files in locations whose paths # have non ASCII characters +def get_resources(zfp, name_or_list_of_names): + ''' + Load resources from the plugin zip file + + :param name_or_list_of_names: List of paths to resources in the zip file using / as + separator, or a single path + + :return: A dictionary of the form ``{name : file_contents}``. Any names + that were not found in the zip file will not be present in the + dictionary. If a single path is passed in the return value will + be just the bytes of the resource or None if it wasn't found. + ''' + names = name_or_list_of_names + if isinstance(names, basestring): + names = [names] + ans = {} + with zipfile.ZipFile(zfp) as zf: + for name in names: + try: + ans[name] = zf.read(name) + except: + pass + if len(names) == 1: + ans = ans.pop(names[0], None) + + return ans + +def get_icons(zfp, name_or_list_of_names): + ''' + Load icons from the plugin zip file + + :param name_or_list_of_names: List of paths to resources in the zip file using / as + separator, or a single path + + :return: A dictionary of the form ``{name : QIcon}``. Any names + that were not found in the zip file will be null QIcons. + If a single path is passed in the return value will + be A QIcon. + ''' + from PyQt4.Qt import QIcon, QPixmap + names = name_or_list_of_names + ans = get_resources(zfp, names) + if isinstance(names, basestring): + names = [names] + if ans is None: + ans = {} + if isinstance(ans, basestring): + ans = dict([(names[0], ans)]) + + ians = {} + for name in names: + p = QPixmap() + raw = ans.get('name', None) + if raw: + p.loadFromData(raw) + ians[name] = QIcon(p) + if len(names) == 1: + ians = ians.pop(names[0]) + return ians class PluginLoader(object): @@ -33,9 +93,10 @@ class PluginLoader(object): return parts[0], None plugin_name = parts[1] with self._lock: - names = self.loaded_plugins.get(plugin_name, None)[1] + names = self.loaded_plugins.get(plugin_name, None) if names is None: raise ImportError('No plugin named %r loaded'%plugin_name) + names = names[1] fullname = '.'.join(parts[2:]) if not fullname: fullname = '__init__' @@ -77,6 +138,8 @@ class PluginLoader(object): with zipfile.ZipFile(zfp) as zf: code = zf.read(zinfo) compiled = compile(code, 'import_name', 'exec', dont_inherit=True) + mod.__dict__['get_resources'] = partial(get_resources, zfp) + mod.__dict__['get_icons'] = partial(get_icons, zfp) exec compiled in mod.__dict__ return mod @@ -139,10 +202,6 @@ class PluginLoader(object): if plugin_name not in self.loaded_plugins: break else: - if plugin_name in self.loaded_plugins: - raise InvalidPlugin(( - 'The plugin in %r uses an import name %r that is already' - ' used by another plugin') % (path_to_zip_file, plugin_name)) if self._identifier_pat.match(plugin_name) is None: raise InvalidPlugin(( 'The plugin at %r uses an invalid import name: %r' % @@ -194,3 +253,18 @@ loader = PluginLoader() sys.meta_path.insert(0, loader) +if __name__ == '__main__': + from tempfile import NamedTemporaryFile + from calibre.customize.ui import add_plugin + from calibre import CurrentDir + path = sys.argv[-1] + with NamedTemporaryFile(suffix='.zip') as f: + with zipfile.ZipFile(f, 'w') as zf: + with CurrentDir(path): + for x in os.listdir('.'): + if x[0] != '.': + print ('Adding', x) + zf.write(x) + add_plugin(f.name) + print ('Added plugin from', sys.argv[-1]) + diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 03aae37883..a0aa0138bc 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -382,6 +382,9 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ error_dialog(self, _('Failed to start content server'), unicode(self.content_server.exception)).exec_() + @property + def current_db(self): + return self.library_view.model().db def another_instance_wants_to_talk(self): try: diff --git a/src/calibre/manual/creating_plugins.rst b/src/calibre/manual/creating_plugins.rst new file mode 100644 index 0000000000..ea1d519864 --- /dev/null +++ b/src/calibre/manual/creating_plugins.rst @@ -0,0 +1,60 @@ + +.. include:: global.rst + +.. _pluginstutorial: + +Writing your own plugins to extend |app|'s functionality +==================================================================== + +|app| has a very modular design. Almost all functionality in |app| comes in the form of plugins. Plugins are used for conversion, for downloading news (though these are called recipes), for various components of the user interface, to connect to different devices, to process files when adding them to |app| and so on. You can get a complete list of all the builtin plugins in |app| by going to :guilabel:`Preferences->Plugins`. + +Here, we will teach you how to create your own plugins to add new features to |app|. + + +.. contents:: Contents + :depth: 2 + :local: + +.. note:: This only applies to calibre releases >= 0.7.52 + +Anatomy of a |app| plugin +--------------------------- + +A |app| plugin is very simple, it's just a zip file that contains some python code +and any other resources like image files needed by the plugin. Without further ado, +let's see a basic example. + +Suppose you have an installation of |app| that you are using to self publish various e-documents in EPUB and MOBI +formats. You would like all files generated by |app| to have their publisher set as "Hello world", here's how to do it. +Create a file named :file:`__init__.py` (this is a special name and must always be used for the main file of your plugin) +and enter the following Python code into it: + +.. literalinclude:: plugin_examples/helloworld/__init__.py + :lines: 10- + +That's all. To add this code to |app| as a plugin, simply create a zip file with:: + + zip plugin.zip __init__.py + +Add this plugin to |app| via :guilabel:`Preferences->Plugins`. + +You can download the Hello World plugin from +`helloworld_plugin.zip `_. + +Every time you use calibre to convert a book, the plugin's :meth:`run` method will be called and the +converted book will have its publisher set to "Hello World". This is a trivial plugin, lets move on to +a more complex example that actually adds a component to the user interface. + +A User Interface plugin +------------------------- + +This plugin will be spread over a couple of files (to keep the code clean). It will show you how to get resources +(images or data files) from the plugin zip file, how to create elements in the |app| user interface and how to access +and query the books database in |app|. + +The different types of plugins +-------------------------------- + +As you may have noticed above, a plugin in |app| is a class. There are different classes for the different types of plugins in |app|. +Details on each class, including the base class of all plugins can be found in :ref:`plugins`. + diff --git a/src/calibre/manual/customize.rst b/src/calibre/manual/customize.rst index 6218bf8112..fe33100576 100644 --- a/src/calibre/manual/customize.rst +++ b/src/calibre/manual/customize.rst @@ -17,6 +17,11 @@ use *plugins* to add functionality to |app|. :depth: 2 :local: +.. toctree:: + :hidden: + + plugins + Environment variables ----------------------- @@ -53,148 +58,10 @@ You should not change the files in this resources folder, as your changes will g For example, if you wanted to change the icon for the :guilabel:`Remove books` action, you would first look in the builtin resources folder and see that the relevant file is :file:`resources/images/trash.svg`. Assuming you have an alternate icon in svg format called :file:`mytrash.svg` you would save it in the configuration directory as :file:`resources/images/trash.svg`. All the icons used by the calibre user interface are in :file:`resources/images` and its sub-folders. -A Hello World plugin ------------------------- +Customizing |app| with plugins +-------------------------------- -Suppose you have an installation of |app| that you are using to self publish various e-documents in EPUB and LRF -format. You would like all file generated by |app| to have their publisher set as "Hello world", here's how to do it. -Create a file name :file:`my_plugin.py` (the file name must end with plugin.py) and enter the following Python code into it: +|app| has a very modular design. Almost all functionality in |app| comes in the form of plugins. Plugins are used for conversion, for downloading news (though these are called recipes), for various components of the user interface, to connect to different devices, to process files when adding them to |app| and so on. You can get a complete list of all the builtin plugins in |app| by going to :guilabel:`Preferences->Plugins`. -.. code-block:: python +You can write your own plugins to customize and extend the behavior of |app|. The plugin architecture in |app| is very simple, see the tutorial :ref:`pluginstutorial`. - import os - from calibre.customize import FileTypePlugin - - class HelloWorld(FileTypePlugin): - - name = 'Hello World Plugin' # Name of the plugin - description = 'Set the publisher to Hello World for all new conversions' - supported_platforms = ['windows', 'osx', 'linux'] # Platforms this plugin will run on - author = 'Acme Inc.' # The author of this plugin - version = (1, 0, 0) # The version number of this plugin - file_types = set(['epub', 'lrf']) # The file types that this plugin will be applied to - on_postprocess = True # Run this plugin after conversion is complete - - def run(self, path_to_ebook): - from calibre.ebooks.metadata.meta import get_metadata, set_metadata - file = open(path_to_ebook, 'r+b') - ext = os.path.splitext(path_to_ebook)[-1][1:].lower() - mi = get_metadata(file, ext) - mi.publisher = 'Hello World' - set_metadata(file, mi, ext) - return path_to_ebook - -That's all. To add this code to |app| as a plugin, simply create a zip file with:: - - zip plugin.zip my_plugin.py - -You can download the Hello World plugin from -`helloworld_plugin.zip `_. -Now either use the configuration dialog in |app| GUI to add this zip file as a plugin, or -use the command:: - - calibre-customize -a plugin.zip - -Every time you use calibre to convert a book, the plugin's :meth:`run` method will be called and the -converted book will have its publisher set to "Hello World". For more information about -|app|'s plugin system, read on... - - -A Hello World GUI plugin ---------------------------- - -Here's a simple Hello World plugin for the |app| GUI. It will cause a box to popup with the message "Hellooo World!" when you press Ctrl+Shift+H - -.. note:: Only available in calibre versions ``>= 0.7.32``. - -.. code-block:: python - - from calibre.customize import InterfaceActionBase - - class HelloWorldBase(InterfaceActionBase): - - name = 'Hello World GUI' - author = 'The little green man' - - def load_actual_plugin(self, gui): - from calibre.gui2.actions import InterfaceAction - - class HelloWorld(InterfaceAction): - name = 'Hello World GUI' - action_spec = ('Hello World!', 'add_book.png', None, - _('Ctrl+Shift+H')) - - def genesis(self): - self.qaction.triggered.connect(self.hello_world) - - def hello_world(self, *args): - from calibre.gui2 import info_dialog - info_dialog(self.gui, 'Hello World!', 'Hellooo World!', - show=True) - - return HelloWorld(gui, self.site_customization) - -You can also have it show up in the toolbars/context menu by going to Preferences->Toolbars and adding this plugin to the locations you want it to be in. - -While this plugin is utterly useless, note that all calibre GUI actions like adding/saving/removing/viewing/etc. are implemented as plugins, so there is no limit to what you can achieve. The key thing to remember is that the plugin has access to the full |app| GUI via ``self.gui``. - - -The Plugin base class ------------------------- - -As you may have noticed above, all |app| plugins are classes. The Plugin classes are organized in a hierarchy at the top of which -is :class:`calibre.customize.Plugin`. The has excellent in source documentation for its various features, here I will discuss a -few of the important ones. - -First, all plugins must supply a list of platforms they have been tested on by setting the ``supported_platforms`` member as in the -example above. - -If the plugin needs to do any initialization, it should implement the :meth:`initialize` method. The path to the plugin zip file -is available as ``self.plugin_path``. The initialization method could be used to load any needed resources from the zip file. - -If the plugin needs to be customized (i.e. it needs some information from the user), it should implement the :meth:`customization_help` -method, to indicate to |app| that it needs user input. This can be useful, for example, to ask the user to input the path to a needed system -binary or the URL of a website, etc. When |app| asks the user for the customization information, the string retuned by the :meth:`customization_help` -method is used as help text to le thte user know what information is needed. - -Another useful method is :meth:`temporary_file`, which returns a file handle to an opened temporary file. If your plugin needs to make use -of temporary files, it should use this method. Temporary file cleanup is then taken care of automatically. - -In addition, whenever plugins are run, their zip files are automatically added to the start of ``sys.path``, so you can directly import -any python files you bundle in the zip files. Note that this is not available when the plugin is being initialized, only when it is being run. - -Finally, plugins can have a priority (a positive integer). Higher priority plugins are run in preference tolower priority ones in a given context. -By default all plugins have priority 1. You can change that by setting the member :attr:'priority` in your subclass. - -See :ref:`pluginsPlugin` for details. - -File type plugins -------------------- - -File type plugins are intended to be associated with specific file types (as identified by extension). They can be run on several different occassions. - - * When books/formats are added ot the |app| database (if :attr:`on_import` is set to True). - * Just before an any2whatever converter is run on an input file (if :attr:`on_preprocess` is set to True). - * After an any2whatever converter has run, on the output file (if :attr:`on_postprocess` is set to True). - -File type plugins specify which file types they are associated with by specifying the :attr:`file_types` member as in the above example. -the actual work should be done in the :meth:`run` method, which must return the path to the modified ebook (it can be the same as the original -if the modifcations are done in place). - -See :ref:`pluginsFTPlugin` for details. - -Metadata plugins -------------------- - -Metadata plugins add the ability to read/write metadata from ebook files to |app|. See :ref:`pluginsMetadataPlugin` for details. - -.. toctree:: - :hidden: - - plugins - -Metadata download plugins ----------------------------- - -Metadata download plugins add various sources that |app| uses to download metadata based on title/author/isbn etc. See :ref:`pluginsMetadataSource` -for details. diff --git a/src/calibre/manual/plugin_examples/helloworld/__init__.py b/src/calibre/manual/plugin_examples/helloworld/__init__.py new file mode 100644 index 0000000000..2cb3236b7b --- /dev/null +++ b/src/calibre/manual/plugin_examples/helloworld/__init__.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2011, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import os +from calibre.customize import FileTypePlugin + +class HelloWorld(FileTypePlugin): + + name = 'Hello World Plugin' # Name of the plugin + description = 'Set the publisher to Hello World for all new conversions' + supported_platforms = ['windows', 'osx', 'linux'] # Platforms this plugin will run on + author = 'Acme Inc.' # The author of this plugin + version = (1, 0, 0) # The version number of this plugin + file_types = set(['epub', 'mobi']) # The file types that this plugin will be applied to + on_postprocess = True # Run this plugin after conversion is complete + minimum_calibre_version = (0, 7, 51) + + def run(self, path_to_ebook): + from calibre.ebooks.metadata.meta import get_metadata, set_metadata + file = open(path_to_ebook, 'r+b') + ext = os.path.splitext(path_to_ebook)[-1][1:].lower() + mi = get_metadata(file, ext) + mi.publisher = 'Hello World' + set_metadata(file, mi, ext) + return path_to_ebook + + diff --git a/src/calibre/manual/plugin_examples/interface/__init__.py b/src/calibre/manual/plugin_examples/interface/__init__.py new file mode 100644 index 0000000000..56b2c1247e --- /dev/null +++ b/src/calibre/manual/plugin_examples/interface/__init__.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2011, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +# The class that all Interface Action plugin wrappers must inherit from +from calibre.customize import InterfaceActionBase + +class InterfacePluginDemo(InterfaceActionBase): + ''' + This class is a simple wrapper that provides information about the actual + plugin class. The actual interface plugin class is called InterfacePlugin + and is defined in the ui.py file, as specified in the actual_plugin field + below. + + The reason for having two classes is that it allows the command line + calibre utilities to run without needing to load the GUI libraries. + ''' + name = 'Interface Plugin Demo' + description = 'An advanced plugin demo' + supported_platforms = ['windows', 'osx', 'linux'] + author = 'Kovid Goyal' + version = (1, 0, 0) + minimum_calibre_version = (0, 7, 51) + + #: This field defines the plugin class that contains all the code + #: that actually does something. + actual_plugin = 'calibre_plugins.interface.ui:InterfacePlugin' + diff --git a/src/calibre/manual/plugin_examples/interface/images/icon.png b/src/calibre/manual/plugin_examples/interface/images/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..ad823e2ff4d53c376632ccb0e2fe60d96cc063ea GIT binary patch literal 5785 zcmV;K7G~**P)g@Il=sYFBw5pBmc%b4Il=J;xC0t2o4nd|H;U}JMAu57Yj`AYqjEkMlJ>n z@c%0#5(jAV=RC#!ug&uRK^O%7Jvs@de=f-f(?I$EC~p4ICl6-)eZvHFEF+5P+#UD8 z0!%OgMvngw%noF8{zt*z8WsPeC}z)k#Q5(s6G9ULH*(eAXN0>M8GoNBrvRn@V+-f= zKY#u*`1buf!`H8089sgf%<%2&HwG3K76x8k9tK_>9tJKhP6kd+P6mJg!WMJ$am6Y; zG(P&PtOrs7OyUqehx2zZ%^Lm;9yIVsVUqqIz{rk>z@H~Lpo$Se2oDV=!G8>lT>nup zDE2<5DgQ@N{H;adKPWu8TknH0n{f=o|1XU2aQ>37)Q^&o;4%1$86$nbVi4@+Z>1|q=!*G!IltL_}YYw+hx<^N0~|A6ts$UqWc`_J(26%zyR z?7NH@sR#skr{85{1d6~?5qI}JjFbf~dA>2C;t!~XVi^3j5>&`Q4B%?K56Q%?d0_TG zU{Yl@jD=VRO^Liy?=o;FwLwy432I4(5)@x+6%Ck0|DA{N|9xg;_)mPWC~`O7e}Fgg zfGIlxNX;kGz(io;UVt^#V+eBozW~G_4U9mV0UL(O6$0@AVv-xE8mRyK_b8Itff&eVW(H^OMyP%igBySb)c^VOm*LOfKMa3< z|6u?b{s)M`^q)T{t^pYY!mPkD6GXGH0%;~7A4mf&1X}ahP7 zXy6?_djjk7qb_I8lB(p0Qd)nG{k+4Vc=?hI0H;ZfuWqsII~l1Jnb8T!rezJd-jyByO^kh+Rf$~61wlSA60#345aGf%Omseieq zjeaI+Xd-XwnyAb}dOhdLWxDK&{WjgX?_G+2-$s-<}+Zjrwq z0cf$B&?bT?{AM@Vwl>&+P!yZCNku^@coGVNsi3H!n434N*NR3Dfg@D6vg8ogLqs{pohQY0_jJ7&1GP-Tmh6y!XEERn1XV zbJ+f#ZvDwtp+Daua6I8zo1pp9qYg2DpeNesyQ=0shteL|7R4V54d>cke4TVc#w8S2 z9J9+$JRjRRgcG`Rk3Q7|d={;`87-(Yh}_IF*YlD@H?XJhdHK3sz_*1V-L$WqmWHEo_RO~GthD+(xbc*uR4gq*F=)O=de(FvR#aCL|&2s_)i$X1@ zwjm$-tb+a_yu!C@ZKe0xO22M3=(&>(g+ZwmPW1@2(*V*7N(8el$ItpP3_&+-XBjgE zr8fLZDERqzlmw5zvGrQEUo5gQKC2>5)v{vXSfTtS45CZ}kms7-yQXCIYq+~{<2Osm zROr(&pMo0O0hQKQb(`k-bmE&fPO=IrS129t?U=K|FFNo2z;Z3g;6eO1n75 z)YY3Hpo`bK<+$Y{(IqB<+H=HrjX#Wv22dUyg3{Y(5V@75XPWd0$6DV}bci`94EH&6 zK7@N3@5$iW$(kHY#?T81Q45tdeo-YXm^0jZBg^UwU15>O&=jq-%qf0y;(Y=@4Fqex zuF$n`Ic}CYd?lSp$o^MDYR}dM)&YGbzKhCytXT{z8x5I_#1ab{ABpMF4MWqDby?Qi z(20^5=*EINo9=jy&N?0}HweMmwIg%gjR;LmU~9H)rB=M%HBW^_kCUVvpPuQj)q>T)+FUhB?A@(B3}G>PyygZs z=j9xgyzrXhti}2(=5*32qy|X4NUg$TrHZtRts-QJj457C1J(w~2zdfamB9gb&5b}w z0Nk+T2M4~y<=8Hpal3@Z%{6nAT;Ah|DqE^hS&ih1RbF#2-*DW{X90;d8?|e4KT}0u`KC(T>c3_ ztQCdEQH0@tX7?_4*R*WKwo(LhNuVOsT&0Ni#T1m5zNDcFLZRfr7iqPPP<)U^t1m4T z4EDhXAEJF0IekzcD$Sd3UZ5ZdX>u)C#42}bN%oq%-Esbzox9o1Zf^H(lP>)1-puaI ze>3yX{Qvj;eXrU2>!T&EXT|z;k%ZJ)yspOeJY?NmTopN_)fpwnEf^|@J0*lp9@f!ScArI`P#~fQQ;w(2i>{ct2Wp-^WvR;uOtyI z?hsWTeNJm-w|1Oa8@GRvfnol*?LX=9V7caamN4&-VEVh-zLgKGc4BjY%))M71pczxV8ENt8v~@4= zT>>x7OtL?u?fkJuirdq}(<3D=mF$>|-_w+VBkS1yE$pKwW8!<_C&qsEvGI&F0x|Q8 z+<@$K#}p|W^I1oRKmQsMz_?GQcip%%8*l0p?Vsa?x>pNBPFX=`h zg+Y;Q5mpx^s2pMi7~u-oC*&RMGqIwN&W@DGPA1l6n5Z?onc#lL5SPNpwn@6~0@!CV zyz0H-3b9xzi2N01mi@=-r$v9+gFj*ItK%Eq)7pjX_-V+V`pj1rxaJ41FCFbVqL^Pr z_qipJr9(TCePxPQ{)4$TV1Is40xn~x{2%W*AdLdsIX?`|?=MMQia+B$Gj>!cxpw)8 zC_|tv|JH}WB`~gYi;>18SkI8NTRH4lr>343b_(U~>o0qIAudLu45Kp)M%zF=D}Uto z4~!m8rXAOoi1QzPPlH z5B85&gCknybtLvax_WVViazbmbv~%!U9y84%$%W_pbm7u+ zIUSJq+7QF7h@}kjh9yw+7c;qA<2-K-Qu2h9OdN$i$t!mE#9&n}WiG}1#VSGJ&kM*1 z#*}&OyQ0@tb=#ow#_WbX9Th=d&$^3-8RY;=OlN!N9U6zq-gxPr!2nM#Uk`zl(_ zZ4eq)Itf36;sE zF(igyK_pYsW=Iq}B9e&PL<2T~cx_{yTn~%NOYxTnfIKEW6ri7QBhNm9t$SrmDI>G#x|dNM_VR+PCZd%nM$YY zOg-)($O$ZLwvY1oSn^DWwFiyKXHX$LlWtHkjH&8nAtKlGO_(zO3sBCr$3{_v@o)C_ zp1rn53@R$L*ET9Das&gYF|-J%G3pT|h+-&ei1B^#8H4l=iAqdp0gd62w8Tez#I%@b zlw#V7h~T3o6>Xpou@FK0<66qK+q>O$9=m&c+g`6n_j;{PGP&Kk*`1x=eDlpW-+bSP z#$(u0rSOuHTH8&c-&zud{(3t|f1wuKw=&P(Y9gB%D4><~Ln$+XP#RUBMgpH3sS46LG8J-#19Ur%~=_N*eNRvwlD$2=8FM;d6em7S}C|7_h7*D zWw83<#-s_)+zn>@gn7*k(mRiqN+IFdG8XK}8lU zug6dY0cwa9D={{XB6Xjeao#4Ab*z8*_PAUu1OCI2?=>A%Q*{l!4-|WVlPfLLB&z+* zuqtp&nG7yJQzXYg6AM0lJGSpd;r50s@Yr=hL$$PJ(E@Pnft}briy;3P(FpP6nB9&?~l7{@iIox1b^qi_&HCOf!h}#v=-Py3;I+3@j zeNPqz@go&n9*d|m;)q?f7_4)b1G;i4BcARAH8=ye~2v{^_#%DSV|!ts?l}q$KVv@FR)53@YdZ$D=Oj2EXMT0DuY)jncsKhfyBi!UchLb5zugQ% z(>8Dg4$*oPu=ifXg8g}Lt$l|9i2)KmdIh4b4PcvhH`rD&3H^6U#A1gl~LCG32JK07i}RKyCox1K#rUKGyJa`Eg@UCRipzWX$a<8KRs>(wrroiDa(6pf}{!;~k=;ETt<#{)!7 z?CG^8`W$Uw)<-{mPC=&6=Hy`t^;1AeS!CA}n(!~|{{ZB!A6VPIC8;+I2+QOMDlr}n zW8YD6Fh%M`Rn*D|Gk$8*l_HDXKeA3up9jtrJE`p#DYs0!E}>CxRh?yHUZ~ugI|A?plZx#{KN_$C9y~qPaPz|QouI%VX(}93>*Pw zuO^>79Pcjy_3Ur7p7q-0V41yKt4I0b&mU7tgWl6am|WcOE`?GC5pUSjfWI4m%ROLS zu!4EJl5hFD4WfJ2gYez^sTQ*u(s%b;o#BivRBAf^Qc*RQRm;aLtBQ)P-NX}lt8zAV z2F6*WlEY+rU8uH++60CXGsr=*`JN18mW$xTxGJg7uk%(TE0lAd3$US*8D+{bnD_}n zC}(3=Fyo+}U+-<;oN_60r#~1(SuCBKJjxaY*t(nzUCS~CY9#2b#amN9QShb(rlgqv zij!8i5^{w`PqFFR_^puz)FcWPa)?%_WEQ{5LIc3iBa;L`2U%FeY-|_DnEp`m9@qM= zs*Fzm5h;12*Mnhf##`7z0`4T4?kyOGGS|AUuxT8_?9m zE29e{k$I6Sx%R45U6dxtboZG3Li3G`(WJ{hvmwX?r4}URT{7%uJd7$Q$SI~TgA~|^ z+lyPMAZnrF8xPXYsx8$(WkETNKB90sX_+H%qApwg%Bqh6?syH+{EVO#B3t1(xE@k7 zxu@mHp9y6X(5zFcMzPxpnhjHvu!=Ir(G(bRaRC~o#Uw!%d*`cvp_A> zb*@zT%{Kr=u06?O5y^l3f;S4ifMzUAfBXAq6QJBH0HzctxEN^Dky+x06@IRhPoYQS zzlj2WMnF{^HV6<4jew}ns3zcQnb8bfUk3CV62cM1>jUU7(ivzCM9ri;UXDO-X0ES; zhWUSJ*dzV>4Fl7p{5HZcaq;QF&#C5OV@h%M=@X>Lft^*7=U8 z4g~slNyLNCq&{1E>Rt+^d1D5X7T!safxM0Q6odr%t8HJ;d$&QNp{?NDejX zr;+V!i29`@15+RDiUE}fr0P|nzt#h&d17c;d5>f$SG+QqLXM8`N_{53cI-q7DQC#j8ViZ2(v+(}AlFvH@4jNBRCW(pr_^LXj*?OdJD>p`M_Q;plKN Date: Sun, 27 Mar 2011 10:59:38 -0600 Subject: [PATCH 04/37] Windows build: Try clearing the zipimport cache if importing modules in the worker process fails --- src/calibre/utils/ipc/worker.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/calibre/utils/ipc/worker.py b/src/calibre/utils/ipc/worker.py index 9594f64ae4..a891d09f3d 100644 --- a/src/calibre/utils/ipc/worker.py +++ b/src/calibre/utils/ipc/worker.py @@ -12,6 +12,8 @@ from threading import Thread from Queue import Queue from contextlib import closing from binascii import unhexlify +from zipimport import ZipImportError + from calibre import prints from calibre.constants import iswindows, isosx @@ -75,7 +77,14 @@ class Progress(Thread): def get_func(name): module, func, notification = PARALLEL_FUNCS[name] - module = importlib.import_module(module) + try: + module = importlib.import_module(module) + except ZipImportError: + # Something windows weird happened, try clearing the zip import cache + # incase the zipfile was changed from under us + from zipimport import _zip_directory_cache as zdc + zdc.clear() + module = importlib.import_module(module) func = getattr(module, func) return func, notification From 03cc260fe5a2ff6ae3aa9e16271da10f52555f7d Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 27 Mar 2011 11:00:54 -0600 Subject: [PATCH 05/37] Complete the example plugin and plugin creating tutorial --- setup/upload.py | 3 + src/calibre/customize/zipplugin.py | 17 ++- src/calibre/manual/creating_plugins.rst | 100 ++++++++++++++++- .../manual/plugin_examples/interface/ui.py | 30 ------ .../{interface => interface_demo}/__init__.py | 7 +- .../plugin_examples/interface_demo/about.txt | 7 ++ .../images/icon.png | Bin .../plugin_examples/interface_demo/main.py | 102 ++++++++++++++++++ .../plugin-import-name-interface_demo.txt} | 0 .../plugin_examples/interface_demo/ui.py | 58 ++++++++++ 10 files changed, 286 insertions(+), 38 deletions(-) delete mode 100644 src/calibre/manual/plugin_examples/interface/ui.py rename src/calibre/manual/plugin_examples/{interface => interface_demo}/__init__.py (79%) create mode 100644 src/calibre/manual/plugin_examples/interface_demo/about.txt rename src/calibre/manual/plugin_examples/{interface => interface_demo}/images/icon.png (100%) create mode 100644 src/calibre/manual/plugin_examples/interface_demo/main.py rename src/calibre/manual/plugin_examples/{interface/plugin-import-name-interface.txt => interface_demo/plugin-import-name-interface_demo.txt} (100%) create mode 100644 src/calibre/manual/plugin_examples/interface_demo/ui.py diff --git a/setup/upload.py b/setup/upload.py index 468c0a61b3..6cd9ad3eca 100644 --- a/setup/upload.py +++ b/setup/upload.py @@ -348,6 +348,9 @@ class UploadUserManual(Command): # {{{ with ZipFile(f, 'w') as zf: for x in os.listdir('.'): zf.write(x) + if os.path.isdir(x): + for y in os.listdir(x): + zf.write(os.path.join(x, y)) bname = self.b(path) + '_plugin.zip' subprocess.check_call(['scp', f.name, 'divok:%s/%s'%(DOWNLOADS, bname)]) diff --git a/src/calibre/customize/zipplugin.py b/src/calibre/customize/zipplugin.py index 932337f624..7f23cf46e2 100644 --- a/src/calibre/customize/zipplugin.py +++ b/src/calibre/customize/zipplugin.py @@ -40,7 +40,8 @@ def get_resources(zfp, name_or_list_of_names): try: ans[name] = zf.read(name) except: - pass + import traceback + traceback.print_exc() if len(names) == 1: ans = ans.pop(names[0], None) @@ -71,7 +72,7 @@ def get_icons(zfp, name_or_list_of_names): ians = {} for name in names: p = QPixmap() - raw = ans.get('name', None) + raw = ans.get(name, None) if raw: p.loadFromData(raw) ians[name] = QIcon(p) @@ -136,8 +137,13 @@ class PluginLoader(object): raise ImportError('Plugin %r has no module named %r' % (plugin_name, import_name)) with zipfile.ZipFile(zfp) as zf: - code = zf.read(zinfo) - compiled = compile(code, 'import_name', 'exec', dont_inherit=True) + try: + code = zf.read(zinfo) + except: + # Maybe the zip file changed from under us + code = zf.read(zinfo.filename) + compiled = compile(code, 'calibre_plugins.%s.%s'%(plugin_name, + import_name), 'exec', dont_inherit=True) mod.__dict__['get_resources'] = partial(get_resources, zfp) mod.__dict__['get_icons'] = partial(get_icons, zfp) exec compiled in mod.__dict__ @@ -265,6 +271,9 @@ if __name__ == '__main__': if x[0] != '.': print ('Adding', x) zf.write(x) + if os.path.isdir(x): + for y in os.listdir(x): + zf.write(os.path.join(x, y)) add_plugin(f.name) print ('Added plugin from', sys.argv[-1]) diff --git a/src/calibre/manual/creating_plugins.rst b/src/calibre/manual/creating_plugins.rst index ea1d519864..d365f10ed0 100644 --- a/src/calibre/manual/creating_plugins.rst +++ b/src/calibre/manual/creating_plugins.rst @@ -48,13 +48,111 @@ a more complex example that actually adds a component to the user interface. A User Interface plugin ------------------------- -This plugin will be spread over a couple of files (to keep the code clean). It will show you how to get resources +This plugin will be spread over a few files (to keep the code clean). It will show you how to get resources (images or data files) from the plugin zip file, how to create elements in the |app| user interface and how to access and query the books database in |app|. +You can download this plugin from `interface_demo_plugin.zip `_ + +The first thing to note is that this zip file has a lot more files in it, explained below, pay particular attention to +``plugin-import-name-interface_demo.txt``. + + **plugin-import-name-interface_demo.txt** + An empty text file used to enable the multi-file plugin magic. This file must be present in all plugins that use + more than one .py file. It should be empty and its filename must be of the form: plugin-import-name-**some_name**.txt + The presence of this file allows you to import code from the .py files present inside the zip file, using a statement like:: + + from calibre_plugins.some_name.some_module import some_object + + The prefix ``calibre_plugins`` must always be present. ``some_name`` comes from the filename of the empty text file. + ``some_module`` refers to :file:`some_module.py` file inside the zip file. Note that this importing is just as + powerful as regular python imports. You can create packages and subpackages of .py modules inside the zip file, + just like you would normally (by defining __init__.py in each sub directory), and everything should Just Work. + + The name you use for ``some_name`` enters a global namespace shared by all plugins, **so make it as unique as possible**. + But remember that it must be a valid python identifier (only alphabets, numbers and the underscore). + + **__init__.py** + As before, the file that defines the plugin class + + **main.py** + This file contains the actual code that does something useful + + **ui.py** + This file defines the interface part of the plugin + + **images/icon.png** + The icon for this plugin + + **about.txt** + A text file with information about the plugin + +Now let's look at the code. + +__init__.py +^^^^^^^^^^^^^ + +First, the obligatory ``__init__.py`` to define the plugin metadata: + +.. literalinclude:: plugin_examples/interface_demo/__init__.py + :lines: 10- + +The only noteworthy feature is the field :attr:`actual_plugin`. Since |app| has both command line and GUI interfaces, +GUI plugins like this one should not load any GUI libraries in __init__.py. The actual_plugin field does this for you, +by telling |app| that the actual plugin is to be found in another file inside your zip archive, which will only be loaded +in a GUI context. + +Remember that for this to work, you must have a plugin-import-name-some_name.txt file in your plugin zip file, +as discussed above. + + +ui.py +^^^^^^^^ + +Now let's look at ui.py which defines the actual GUI plugin. The source code is heavily commented and should be self explanatory: + +.. literalinclude:: plugin_examples/interface_demo/ui.py + :lines: 10- + +main.py +^^^^^^^^^ + +The actual logic to implement the Interface Plugin Demo dialog. + +.. literalinclude:: plugin_examples/interface_demo/main.py + :lines: 10- + +Getting resources from the plugin zip file +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +|app|'s plugin loading system defines a couple of builtin functions that allow you to conveniently get files from the plugin zip file. + + **get_resources(name_or_list_of_names)** + This function should be called with a list of paths to files inside the zip file. For example to access the file icon.png in + the directory images in the zip file, you would use: ``images/icon.png``. Always use a forward slash as the path separator, + even on windows. When you pass in a single name, the function will return the raw bytes of that file or None if the name + was not found in the zip file. If you pass in more than one name then it returns a dict mapping the names to bytes. + If a name is not found, it will not be present in the returned dict. + + **get_icons(name_or_list_of_names)** + A convenience wrapper for get_resources() that creates QIcon objects from the raw bytes returned by get_resources. + If a name is not found in the zip file the corresponding QIcon will be null. + + The different types of plugins -------------------------------- As you may have noticed above, a plugin in |app| is a class. There are different classes for the different types of plugins in |app|. Details on each class, including the base class of all plugins can be found in :ref:`plugins`. +More plugin examples +---------------------- + +You can find a list of many, sophisticated |app| plugins `here `_. + +Sharing your plugins with others +---------------------------------- + +If you would like to share the plugins you have created with other users of |app|, post your plugin in a new thread in the +`calibre plugins forum `_. + diff --git a/src/calibre/manual/plugin_examples/interface/ui.py b/src/calibre/manual/plugin_examples/interface/ui.py deleted file mode 100644 index dc6e03ce65..0000000000 --- a/src/calibre/manual/plugin_examples/interface/ui.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env python -# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai -from __future__ import (unicode_literals, division, absolute_import, - print_function) - -__license__ = 'GPL v3' -__copyright__ = '2011, Kovid Goyal ' -__docformat__ = 'restructuredtext en' - - -# The class that all interface action plugins must inherit from -from calibre.gui2.actions import InterfaceAction - -if False: - # This is here to keep my python error checker from complaining about - # the builtins that will be defined by the plugin loading system - get_icons = None - -class InterfacePlugin(InterfaceAction): - - name = 'Interface Plugin Demo' - - action_spec = ('Interface Plugin Demo', None, - 'Run the Interface Plugin Demo', 'Ctrl+Shift+F1') - - def genesis(self): - # This method is called once per plugin, do initial setup here - print (1111, get_icons('icon.png')) - self.qaction.setIcon(get_icons('icon.png')) - diff --git a/src/calibre/manual/plugin_examples/interface/__init__.py b/src/calibre/manual/plugin_examples/interface_demo/__init__.py similarity index 79% rename from src/calibre/manual/plugin_examples/interface/__init__.py rename to src/calibre/manual/plugin_examples/interface_demo/__init__.py index 56b2c1247e..dc33f4e427 100644 --- a/src/calibre/manual/plugin_examples/interface/__init__.py +++ b/src/calibre/manual/plugin_examples/interface_demo/__init__.py @@ -27,7 +27,8 @@ class InterfacePluginDemo(InterfaceActionBase): version = (1, 0, 0) minimum_calibre_version = (0, 7, 51) - #: This field defines the plugin class that contains all the code - #: that actually does something. - actual_plugin = 'calibre_plugins.interface.ui:InterfacePlugin' + #: This field defines the GUI plugin class that contains all the code + #: that actually does something. Its format is module_path:class_name + #: The specified class must be defined in the specified module. + actual_plugin = 'calibre_plugins.interface_demo.ui:InterfacePlugin' diff --git a/src/calibre/manual/plugin_examples/interface_demo/about.txt b/src/calibre/manual/plugin_examples/interface_demo/about.txt new file mode 100644 index 0000000000..8ab957e322 --- /dev/null +++ b/src/calibre/manual/plugin_examples/interface_demo/about.txt @@ -0,0 +1,7 @@ +The Interface Plugin Demo +=========================== + +Created by Kovid Goyal + +Requires calibre >= 0.7.52 + diff --git a/src/calibre/manual/plugin_examples/interface/images/icon.png b/src/calibre/manual/plugin_examples/interface_demo/images/icon.png similarity index 100% rename from src/calibre/manual/plugin_examples/interface/images/icon.png rename to src/calibre/manual/plugin_examples/interface_demo/images/icon.png diff --git a/src/calibre/manual/plugin_examples/interface_demo/main.py b/src/calibre/manual/plugin_examples/interface_demo/main.py new file mode 100644 index 0000000000..de668799aa --- /dev/null +++ b/src/calibre/manual/plugin_examples/interface_demo/main.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2011, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + + +from PyQt4.Qt import QDialog, QVBoxLayout, QPushButton, QMessageBox + +if False: + # This is here to keep my python error checker from complaining about + # the builtin functions that will be defined by the plugin loading system + get_icons = get_resources = None + +class DemoDialog(QDialog): + + def __init__(self, gui, icon): + QDialog.__init__(self, gui) + self.gui = gui + + # The current database shown in the GUI + # db is an instance of the class LibraryDatabase2 from database.py + # This class has many, many methods that allow you to do a lot of + # things. + self.db = gui.current_db + + self.l = QVBoxLayout() + self.setLayout(self.l) + + + self.setWindowTitle('Interface Plugin Demo') + self.setWindowIcon(icon) + + self.about_button = QPushButton('About', self) + self.about_button.clicked.connect(self.about) + self.l.addWidget(self.about_button) + + self.marked_button = QPushButton( + 'Show books with only one format in the calibre GUI', self) + self.marked_button.clicked.connect(self.marked) + self.l.addWidget(self.marked_button) + + self.view_button = QPushButton( + 'View the most recently added book', self) + self.view_button.clicked.connect(self.view) + self.l.addWidget(self.view_button) + + self.resize(self.sizeHint()) + + def about(self): + # Get the about text from a file inside the plugin zip file + # The get_resources function is a builtin function defined for all your + # plugin code. It loads files from the plugin zip file. It returns + # the bytes from the specified file. + # + # Note that if you are loading more than one file, for performance, you + # should pass a list of names to get_resources. In this case, + # get_resources will return a dictionary mapping names to bytes. Names that + # are not found in the zip file will not be in the returned dictionary. + text = get_resources('about.txt') + QMessageBox.about(self, 'About the Interface Plugin Demo', + text.decode('utf-8')) + + def marked(self): + fmt_idx = self.db.FIELD_MAP['formats'] + matched_ids = set() + for record in self.db.data.iterall(): + # Iterate over all records + fmts = record[fmt_idx] + # fmts is either None or a comma separated list of formats + if fmts and ',' not in fmts: + matched_ids.add(record[0]) + # Mark the records with the matching ids + self.db.set_marked_ids(matched_ids) + + # Tell the GUI to search for all marked records + self.gui.search.setEditText('marked:true') + self.gui.search.do_search() + + def view(self): + most_recent = most_recent_id = None + timestamp_idx = self.db.FIELD_MAP['timestamp'] + + for record in self.db.data: + # Iterate over all currently showing records + timestamp = record[timestamp_idx] + if most_recent is None or timestamp > most_recent: + most_recent = timestamp + most_recent_id = record[0] + + if most_recent_id is not None: + # Get the row number of the id as shown in the GUI + row_number = self.db.row(most_recent_id) + # Get a reference to the View plugin + view_plugin = self.gui.iactions['View'] + # Ask the view plugin to launch the viewer for row_number + view_plugin._view_books([row_number]) + + diff --git a/src/calibre/manual/plugin_examples/interface/plugin-import-name-interface.txt b/src/calibre/manual/plugin_examples/interface_demo/plugin-import-name-interface_demo.txt similarity index 100% rename from src/calibre/manual/plugin_examples/interface/plugin-import-name-interface.txt rename to src/calibre/manual/plugin_examples/interface_demo/plugin-import-name-interface_demo.txt diff --git a/src/calibre/manual/plugin_examples/interface_demo/ui.py b/src/calibre/manual/plugin_examples/interface_demo/ui.py new file mode 100644 index 0000000000..b3535da925 --- /dev/null +++ b/src/calibre/manual/plugin_examples/interface_demo/ui.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2011, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + + +# The class that all interface action plugins must inherit from +from calibre.gui2.actions import InterfaceAction +from calibre_plugins.interface_demo.main import DemoDialog + +if False: + # This is here to keep my python error checker from complaining about + # the builtin functions that will be defined by the plugin loading system + get_icons = get_resources = None + +class InterfacePlugin(InterfaceAction): + + name = 'Interface Plugin Demo' + + # Declare the main action associated with this plugin + # The keyboard shortcut can be None if you dont want to use a keyboard + # shortcut. Remember that currently calibre has no central management for + # keyboard shortcuts, so try to use an unusual/unused shortcut. + action_spec = ('Interface Plugin Demo', None, + 'Run the Interface Plugin Demo', 'Ctrl+Shift+F1') + + def genesis(self): + # This method is called once per plugin, do initial setup here + + # Set the icon for this interface action + # The get_icons function is a builtin function defined for all your + # plugin code. It loads icons from the plugin zip file. It returns + # QIcon objects, if you want the actual data, use the analogous + # get_resources builtin function. + # + # Note that if you are loading more than one icon, for performance, you + # should pass a list of names to get_icons. In this case, get_icons + # will return a dictionary mapping names to QIcons. Names that + # are not found in the zip file will result in null QIcons. + icon = get_icons('images/icon.png') + + # The qaction is automatically created from the action_spec defined + # above + self.qaction.setIcon(icon) + self.qaction.triggered.connect(self.show_dialog) + + def show_dialog(self): + # self.gui is the main calibre GUI. It acts as the gateway to access + # all the elements of the calibre user interface, it should also be the + # parent of the dialog + d = DemoDialog(self.gui, self.qaction.icon()) + d.show() + + From 3546254f5a9263e9c74bf85e8fd05e280ab1b2a4 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 27 Mar 2011 12:10:44 -0600 Subject: [PATCH 06/37] ... --- src/calibre/manual/creating_plugins.rst | 2 +- src/calibre/manual/plugin_examples/interface_demo/about.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/manual/creating_plugins.rst b/src/calibre/manual/creating_plugins.rst index d365f10ed0..4667899d3b 100644 --- a/src/calibre/manual/creating_plugins.rst +++ b/src/calibre/manual/creating_plugins.rst @@ -15,7 +15,7 @@ Here, we will teach you how to create your own plugins to add new features to |a :depth: 2 :local: -.. note:: This only applies to calibre releases >= 0.7.52 +.. note:: This only applies to calibre releases >= 0.7.53 Anatomy of a |app| plugin --------------------------- diff --git a/src/calibre/manual/plugin_examples/interface_demo/about.txt b/src/calibre/manual/plugin_examples/interface_demo/about.txt index 8ab957e322..f35b1e7196 100644 --- a/src/calibre/manual/plugin_examples/interface_demo/about.txt +++ b/src/calibre/manual/plugin_examples/interface_demo/about.txt @@ -3,5 +3,5 @@ The Interface Plugin Demo Created by Kovid Goyal -Requires calibre >= 0.7.52 +Requires calibre >= 0.7.53 From ef4278850e9abd8040a10d82ce29c5bf2d42e2ab Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sun, 27 Mar 2011 19:15:06 +0100 Subject: [PATCH 07/37] Add a compile-time option to a device driver to scan for books from the root instead of the book_dir, while still respecting the book dir for sending books. Fixes problems on Sonys where ADE etc put books in the 'wrong' spot. --- src/calibre/devices/prs505/driver.py | 1 + src/calibre/devices/usbms/driver.py | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/calibre/devices/prs505/driver.py b/src/calibre/devices/prs505/driver.py index da1ef55786..c97c3dc2d8 100644 --- a/src/calibre/devices/prs505/driver.py +++ b/src/calibre/devices/prs505/driver.py @@ -57,6 +57,7 @@ class PRS505(USBMS): MUST_READ_METADATA = True SUPPORTS_USE_AUTHOR_SORT = True EBOOK_DIR_MAIN = 'database/media/books' + SCAN_FROM_ROOT = True ALL_BY_TITLE = _('All by title') ALL_BY_AUTHOR = _('All by author') diff --git a/src/calibre/devices/usbms/driver.py b/src/calibre/devices/usbms/driver.py index 578c28b894..a391b88972 100644 --- a/src/calibre/devices/usbms/driver.py +++ b/src/calibre/devices/usbms/driver.py @@ -55,6 +55,8 @@ class USBMS(CLI, Device): METADATA_CACHE = 'metadata.calibre' DRIVEINFO = 'driveinfo.calibre' + SCAN_FROM_ROOT = False + def _update_driveinfo_record(self, dinfo, prefix, location_code, name=None): if not isinstance(dinfo, dict): dinfo = {} @@ -173,7 +175,10 @@ class USBMS(CLI, Device): ebook_dirs = [ebook_dirs] for ebook_dir in ebook_dirs: ebook_dir = self.path_to_unicode(ebook_dir) - ebook_dir = self.normalize_path( \ + if self.SCAN_FROM_ROOT: + ebook_dir = self.normalize_path(prefix) + else: + ebook_dir = self.normalize_path( \ os.path.join(prefix, *(ebook_dir.split('/'))) \ if ebook_dir else prefix) if not os.path.exists(ebook_dir): continue From cc219f774fc7da5c48bdc2ef841919a6dc74c5db Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 27 Mar 2011 12:29:44 -0600 Subject: [PATCH 08/37] Clarify comment in plugin examples --- src/calibre/manual/creating_plugins.rst | 4 ++-- .../manual/plugin_examples/helloworld/__init__.py | 2 +- .../manual/plugin_examples/interface_demo/__init__.py | 2 +- .../manual/plugin_examples/interface_demo/main.py | 6 +++--- src/calibre/manual/plugin_examples/interface_demo/ui.py | 9 +++++---- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/calibre/manual/creating_plugins.rst b/src/calibre/manual/creating_plugins.rst index 4667899d3b..0e1c4a51a9 100644 --- a/src/calibre/manual/creating_plugins.rst +++ b/src/calibre/manual/creating_plugins.rst @@ -112,7 +112,7 @@ ui.py Now let's look at ui.py which defines the actual GUI plugin. The source code is heavily commented and should be self explanatory: .. literalinclude:: plugin_examples/interface_demo/ui.py - :lines: 10- + :lines: 16- main.py ^^^^^^^^^ @@ -120,7 +120,7 @@ main.py The actual logic to implement the Interface Plugin Demo dialog. .. literalinclude:: plugin_examples/interface_demo/main.py - :lines: 10- + :lines: 16- Getting resources from the plugin zip file ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/calibre/manual/plugin_examples/helloworld/__init__.py b/src/calibre/manual/plugin_examples/helloworld/__init__.py index 2cb3236b7b..b9b693d9a9 100644 --- a/src/calibre/manual/plugin_examples/helloworld/__init__.py +++ b/src/calibre/manual/plugin_examples/helloworld/__init__.py @@ -19,7 +19,7 @@ class HelloWorld(FileTypePlugin): version = (1, 0, 0) # The version number of this plugin file_types = set(['epub', 'mobi']) # The file types that this plugin will be applied to on_postprocess = True # Run this plugin after conversion is complete - minimum_calibre_version = (0, 7, 51) + minimum_calibre_version = (0, 7, 52) def run(self, path_to_ebook): from calibre.ebooks.metadata.meta import get_metadata, set_metadata diff --git a/src/calibre/manual/plugin_examples/interface_demo/__init__.py b/src/calibre/manual/plugin_examples/interface_demo/__init__.py index dc33f4e427..aa9420b5c4 100644 --- a/src/calibre/manual/plugin_examples/interface_demo/__init__.py +++ b/src/calibre/manual/plugin_examples/interface_demo/__init__.py @@ -25,7 +25,7 @@ class InterfacePluginDemo(InterfaceActionBase): supported_platforms = ['windows', 'osx', 'linux'] author = 'Kovid Goyal' version = (1, 0, 0) - minimum_calibre_version = (0, 7, 51) + minimum_calibre_version = (0, 7, 52) #: This field defines the GUI plugin class that contains all the code #: that actually does something. Its format is module_path:class_name diff --git a/src/calibre/manual/plugin_examples/interface_demo/main.py b/src/calibre/manual/plugin_examples/interface_demo/main.py index de668799aa..53c9ae68dc 100644 --- a/src/calibre/manual/plugin_examples/interface_demo/main.py +++ b/src/calibre/manual/plugin_examples/interface_demo/main.py @@ -7,14 +7,14 @@ __license__ = 'GPL v3' __copyright__ = '2011, Kovid Goyal ' __docformat__ = 'restructuredtext en' - -from PyQt4.Qt import QDialog, QVBoxLayout, QPushButton, QMessageBox - if False: # This is here to keep my python error checker from complaining about # the builtin functions that will be defined by the plugin loading system + # You do not need this code in your plugins get_icons = get_resources = None +from PyQt4.Qt import QDialog, QVBoxLayout, QPushButton, QMessageBox + class DemoDialog(QDialog): def __init__(self, gui, icon): diff --git a/src/calibre/manual/plugin_examples/interface_demo/ui.py b/src/calibre/manual/plugin_examples/interface_demo/ui.py index b3535da925..3980291cc4 100644 --- a/src/calibre/manual/plugin_examples/interface_demo/ui.py +++ b/src/calibre/manual/plugin_examples/interface_demo/ui.py @@ -7,15 +7,16 @@ __license__ = 'GPL v3' __copyright__ = '2011, Kovid Goyal ' __docformat__ = 'restructuredtext en' +if False: + # This is here to keep my python error checker from complaining about + # the builtin functions that will be defined by the plugin loading system + # You do not need this code in your plugins + get_icons = get_resources = None # The class that all interface action plugins must inherit from from calibre.gui2.actions import InterfaceAction from calibre_plugins.interface_demo.main import DemoDialog -if False: - # This is here to keep my python error checker from complaining about - # the builtin functions that will be defined by the plugin loading system - get_icons = get_resources = None class InterfacePlugin(InterfaceAction): From 87c7fc955841bd6e5ebe87ae2a510c5e39140e7d Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 27 Mar 2011 13:12:07 -0600 Subject: [PATCH 09/37] Add configuration to example plugin --- .../interface_demo/__init__.py | 30 +++++++++++++ .../plugin_examples/interface_demo/config.py | 42 +++++++++++++++++++ .../plugin_examples/interface_demo/main.py | 18 +++++++- .../plugin_examples/interface_demo/ui.py | 10 ++++- 4 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 src/calibre/manual/plugin_examples/interface_demo/config.py diff --git a/src/calibre/manual/plugin_examples/interface_demo/__init__.py b/src/calibre/manual/plugin_examples/interface_demo/__init__.py index aa9420b5c4..2ad4a0245e 100644 --- a/src/calibre/manual/plugin_examples/interface_demo/__init__.py +++ b/src/calibre/manual/plugin_examples/interface_demo/__init__.py @@ -32,3 +32,33 @@ class InterfacePluginDemo(InterfaceActionBase): #: The specified class must be defined in the specified module. actual_plugin = 'calibre_plugins.interface_demo.ui:InterfacePlugin' + def config_widget(self): + ''' + Implement this method and :meth:`save_settings` in your plugin to + use a custom configuration dialog. + + This method, if implemented, must return a QWidget. The widget can have + an optional method validate() that takes no arguments and is called + immediately after the user clicks OK. Changes are applied if and only + if the method returns True. + + If for some reason you cannot perform the configuration at this time, + return a tuple of two strings (message, details), these will be + displayed as a warning dialog to the user and the process will be + aborted. + + The base class implementation of this method raises NotImplementedError + so by default no user configuration is possible. + ''' + from calibre_plugins.interface_demo.config import ConfigWidget + return ConfigWidget() + + def save_settings(self, config_widget): + ''' + Save the settings specified by the user with config_widget. + + :param config_widget: The widget returned by :meth:`config_widget`. + ''' + config_widget.save_settings() + + diff --git a/src/calibre/manual/plugin_examples/interface_demo/config.py b/src/calibre/manual/plugin_examples/interface_demo/config.py new file mode 100644 index 0000000000..24376ab916 --- /dev/null +++ b/src/calibre/manual/plugin_examples/interface_demo/config.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2011, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +from PyQt4.Qt import QWidget, QHBoxLayout, QLabel, QLineEdit + +from calibre.utils.config import JSONConfig + +# This is where all preferences for this plugin will be stored +# Remember that this name is also ina global namespace, so make it as uniqie +# as possible. +# It is good etiquette, though not strictly required to always prefix your +# config file name with plugins/, so as to ensure you dont accidentally clobber +# a calibre config file +prefs = JSONConfig('plugins/interface_demo') + +# Set defaults +prefs.defaults['hello_world_msg'] = 'Hello, World!' + +class ConfigWidget(QWidget): + + def __init__(self): + QWidget.__init__(self) + self.l = QHBoxLayout() + self.setLayout(self.l) + + self.label = QLabel('Hello world &message:') + self.l.addWidget(self.label) + + self.msg = QLineEdit(self) + self.msg.setText(prefs['hello_world_msg']) + self.l.addWidget(self.msg) + self.label.setBuddy(self.msg) + + def save_settings(self): + prefs['hello_world_msg'] = unicode(self.msg.text()) + diff --git a/src/calibre/manual/plugin_examples/interface_demo/main.py b/src/calibre/manual/plugin_examples/interface_demo/main.py index 53c9ae68dc..f23664b1de 100644 --- a/src/calibre/manual/plugin_examples/interface_demo/main.py +++ b/src/calibre/manual/plugin_examples/interface_demo/main.py @@ -13,13 +13,16 @@ if False: # You do not need this code in your plugins get_icons = get_resources = None -from PyQt4.Qt import QDialog, QVBoxLayout, QPushButton, QMessageBox +from PyQt4.Qt import QDialog, QVBoxLayout, QPushButton, QMessageBox, QLabel + +from calibre_plugins.interface_demo.config import prefs class DemoDialog(QDialog): - def __init__(self, gui, icon): + def __init__(self, gui, icon, do_user_config): QDialog.__init__(self, gui) self.gui = gui + self.do_user_config = do_user_config # The current database shown in the GUI # db is an instance of the class LibraryDatabase2 from database.py @@ -30,6 +33,8 @@ class DemoDialog(QDialog): self.l = QVBoxLayout() self.setLayout(self.l) + self.label = QLabel(prefs['hello_world_msg']) + self.l.addWidget(self.label) self.setWindowTitle('Interface Plugin Demo') self.setWindowIcon(icon) @@ -48,6 +53,11 @@ class DemoDialog(QDialog): self.view_button.clicked.connect(self.view) self.l.addWidget(self.view_button) + self.conf_button = QPushButton( + 'Configure this plugin', self) + self.conf_button.clicked.connect(self.config) + self.l.addWidget(self.conf_button) + self.resize(self.sizeHint()) def about(self): @@ -99,4 +109,8 @@ class DemoDialog(QDialog): # Ask the view plugin to launch the viewer for row_number view_plugin._view_books([row_number]) + def config(self): + self.do_user_config(parent=self) + # Apply the changes + self.label.setText(prefs['hello_world_msg']) diff --git a/src/calibre/manual/plugin_examples/interface_demo/ui.py b/src/calibre/manual/plugin_examples/interface_demo/ui.py index 3980291cc4..9b60a66ed4 100644 --- a/src/calibre/manual/plugin_examples/interface_demo/ui.py +++ b/src/calibre/manual/plugin_examples/interface_demo/ui.py @@ -50,10 +50,18 @@ class InterfacePlugin(InterfaceAction): self.qaction.triggered.connect(self.show_dialog) def show_dialog(self): + # The base plugin object defined in __init__.py + base_plugin_object = self.interface_action_base_plugin + # Show the config dialog + # The config dialog can also be shown from within + # Preferences->Plugins, which is why the do_user_config + # method is defined on the base plugin class + do_user_config = base_plugin_object.do_user_config + # self.gui is the main calibre GUI. It acts as the gateway to access # all the elements of the calibre user interface, it should also be the # parent of the dialog - d = DemoDialog(self.gui, self.qaction.icon()) + d = DemoDialog(self.gui, self.qaction.icon(), do_user_config) d.show() From 199738861a327f0fff165d9f662f61d1c3bfa9f6 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sun, 27 Mar 2011 20:31:57 +0100 Subject: [PATCH 10/37] Make scan_all optional in sony driver, with default True --- src/calibre/devices/prs505/driver.py | 19 +++++++++++++++---- src/calibre/devices/usbms/driver.py | 1 + 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/calibre/devices/prs505/driver.py b/src/calibre/devices/prs505/driver.py index c97c3dc2d8..2bcbb19ac7 100644 --- a/src/calibre/devices/prs505/driver.py +++ b/src/calibre/devices/prs505/driver.py @@ -57,7 +57,7 @@ class PRS505(USBMS): MUST_READ_METADATA = True SUPPORTS_USE_AUTHOR_SORT = True EBOOK_DIR_MAIN = 'database/media/books' - SCAN_FROM_ROOT = True + SCAN_FROM_ROOT = False ALL_BY_TITLE = _('All by title') ALL_BY_AUTHOR = _('All by author') @@ -88,18 +88,27 @@ class PRS505(USBMS): _('Set this option if you want the cover thumbnails to have ' 'the same aspect ratio (width to height) as the cover. ' 'Unset it if you want the thumbnail to be the maximum size, ' - 'ignoring aspect ratio.') + 'ignoring aspect ratio.'), + _('Scan for books in all folders') + + ':::' + + _('Setting this option tells calibre to look for books in all ' + 'folders on the device and its cards. This permits calibre to ' + 'find books put on the device by other software and by ' + 'wireless download.') ] EXTRA_CUSTOMIZATION_DEFAULT = [ ', '.join(['series', 'tags']), False, False, + True, True ] OPT_COLLECTIONS = 0 OPT_UPLOAD_COVERS = 1 OPT_REFRESH_COVERS = 2 + OPT_PRESERVE_ASPECT_RATIO = 3 + OPT_SCAN_FROM_ROOT = 4 plugboard = None plugboard_func = None @@ -148,11 +157,13 @@ class PRS505(USBMS): self.booklist_class.rebuild_collections = self.rebuild_collections # Set the thumbnail width to the theoretical max if the user has asked # that we do not preserve aspect ratio - if not self.settings().extra_customization[3]: + if not self.settings().extra_customization[self.OPT_PRESERVE_ASPECT_RATIO]: self.THUMBNAIL_WIDTH = 168 # Set WANTS_UPDATED_THUMBNAILS if the user has asked that thumbnails be # updated on every connect - self.WANTS_UPDATED_THUMBNAILS = self.settings().extra_customization[2] + self.WANTS_UPDATED_THUMBNAILS = \ + self.settings().extra_customization[self.OPT_REFRESH_COVERS] + self.SCAN_FROM_ROOT = self.settings().extra_customization[self.OPT_SCAN_FROM_ROOT] def filename_callback(self, fname, mi): if getattr(mi, 'application_id', None) is not None: diff --git a/src/calibre/devices/usbms/driver.py b/src/calibre/devices/usbms/driver.py index a391b88972..c8580f6741 100644 --- a/src/calibre/devices/usbms/driver.py +++ b/src/calibre/devices/usbms/driver.py @@ -181,6 +181,7 @@ class USBMS(CLI, Device): ebook_dir = self.normalize_path( \ os.path.join(prefix, *(ebook_dir.split('/'))) \ if ebook_dir else prefix) + debug_print('USBMS: scan from root', self.SCAN_FROM_ROOT, ebook_dir) if not os.path.exists(ebook_dir): continue # Get all books in the ebook_dir directory if self.SUPPORTS_SUB_DIRS: From a3e095b2c62d30c9dbe5e7272b1c2887fc05d092 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 27 Mar 2011 13:34:23 -0600 Subject: [PATCH 11/37] Add config_widget docs to creating_plugins.rst --- src/calibre/manual/creating_plugins.rst | 29 +++++++++++++++++++ .../plugin_examples/interface_demo/config.py | 7 ++--- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/calibre/manual/creating_plugins.rst b/src/calibre/manual/creating_plugins.rst index 0e1c4a51a9..65e44760ef 100644 --- a/src/calibre/manual/creating_plugins.rst +++ b/src/calibre/manual/creating_plugins.rst @@ -105,6 +105,7 @@ in a GUI context. Remember that for this to work, you must have a plugin-import-name-some_name.txt file in your plugin zip file, as discussed above. +Also there are a couple of methods for enabling user configuration of the plugin. These are discussed below. ui.py ^^^^^^^^ @@ -138,6 +139,34 @@ Getting resources from the plugin zip file A convenience wrapper for get_resources() that creates QIcon objects from the raw bytes returned by get_resources. If a name is not found in the zip file the corresponding QIcon will be null. +Enabling user configuration of your plugin +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To allow users to configure your plugin, you must define a couple of methods in your base plugin class, **config_widget** and **save_settings** as shown below: + +.. literalinclude:: plugin_examples/interface_demo/__init__.py + :pyobject: InterfacePluginDemo.config_widget + +.. literalinclude:: plugin_examples/interface_demo/__init__.py + :pyobject: InterfacePluginDemo.save_settings + +|app| has many different ways to store configuration data (a legacy of its long history). The recommended way is to use the **JSONConfig** class, which stores your configuration information in a .json file. + +The code to manage configuration data in the demo plugin is in config.py: + +.. literalinclude:: plugin_examples/interface_demo/config.py + :lines: 10- + +The ``prefs`` object is now available throughout the plugin code by a simple:: + + from calibre_plugins.interface_demo.config import prefs + + +You can see the ``prefs`` object being used in main.py: + +.. literalinclude:: plugin_examples/interface_demo/main.py + :pyobject: DemoDialog.config + The different types of plugins -------------------------------- diff --git a/src/calibre/manual/plugin_examples/interface_demo/config.py b/src/calibre/manual/plugin_examples/interface_demo/config.py index 24376ab916..e718106d3e 100644 --- a/src/calibre/manual/plugin_examples/interface_demo/config.py +++ b/src/calibre/manual/plugin_examples/interface_demo/config.py @@ -12,11 +12,10 @@ from PyQt4.Qt import QWidget, QHBoxLayout, QLabel, QLineEdit from calibre.utils.config import JSONConfig # This is where all preferences for this plugin will be stored -# Remember that this name is also ina global namespace, so make it as uniqie +# Remember that this name is also in a global namespace, so make it as unique # as possible. -# It is good etiquette, though not strictly required to always prefix your -# config file name with plugins/, so as to ensure you dont accidentally clobber -# a calibre config file +# You should always prefix your config file name with plugins/, +# so as to ensure you dont accidentally clobber a calibre config file prefs = JSONConfig('plugins/interface_demo') # Set defaults From 4dee0cdd6bae01a136d9a88aa71d8c238fd89ac6 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 27 Mar 2011 13:49:17 -0600 Subject: [PATCH 12/37] ... --- src/calibre/manual/plugin_examples/interface_demo/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/manual/plugin_examples/interface_demo/config.py b/src/calibre/manual/plugin_examples/interface_demo/config.py index e718106d3e..fd391ce944 100644 --- a/src/calibre/manual/plugin_examples/interface_demo/config.py +++ b/src/calibre/manual/plugin_examples/interface_demo/config.py @@ -12,8 +12,8 @@ from PyQt4.Qt import QWidget, QHBoxLayout, QLabel, QLineEdit from calibre.utils.config import JSONConfig # This is where all preferences for this plugin will be stored -# Remember that this name is also in a global namespace, so make it as unique -# as possible. +# Remember that this name (i.e. plugins/interface_demo) is also +# in a global namespace, so make it as unique as possible. # You should always prefix your config file name with plugins/, # so as to ensure you dont accidentally clobber a calibre config file prefs = JSONConfig('plugins/interface_demo') From 8e2ca7681f689c389fa8c7a4a78a10b67b92e5a4 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 27 Mar 2011 14:12:35 -0600 Subject: [PATCH 13/37] Add a section on how to debug plugins --- src/calibre/manual/creating_plugins.rst | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/calibre/manual/creating_plugins.rst b/src/calibre/manual/creating_plugins.rst index 65e44760ef..90494231d2 100644 --- a/src/calibre/manual/creating_plugins.rst +++ b/src/calibre/manual/creating_plugins.rst @@ -174,6 +174,27 @@ The different types of plugins As you may have noticed above, a plugin in |app| is a class. There are different classes for the different types of plugins in |app|. Details on each class, including the base class of all plugins can be found in :ref:`plugins`. +Debugging plugins +------------------- + +The first, most important step is to run |app| in debug mode. You can do this from the command line with:: + + calibre-debug -g + +Or from within calibre by clicking the arrow next to the preferences button or using the `Ctrl+Shift+R` keyboard shortcut. + +When running from the command line, debug output will be printed to the console, when running from within |app| the output will go to a txt file. + +You can insert print statements anywhere in your plugin code, they will be output in debug mode. Remember, this is python, you really shouldn't need anything more than print statements to debug ;) I developed all of |app| using just this debugging technique. + +It can get tiresome to keep re-adding a plugin to calibre to test small changes. The plugin zip files are stored in the calibre config directory in plugins/ (goto Preferences->Misc and click open config directory to see the config directory). + +Once you've located the zip file of your plugin you can then directly update it with your changes instead of re-adding it each time. To do so from the command line, in the directory that contains your plugin source code, use:: + + zip -R /path/to/plugin/zip/file.zip * + +This will automatically update all changed files. It relies on the freely available zip command line tool. + More plugin examples ---------------------- From 157155d774e1366664b68461171c023ed2b60710 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 27 Mar 2011 16:38:23 -0600 Subject: [PATCH 14/37] ... --- src/calibre/manual/creating_plugins.rst | 5 ++++- .../manual/plugin_examples/interface_demo/__init__.py | 7 +++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/calibre/manual/creating_plugins.rst b/src/calibre/manual/creating_plugins.rst index 90494231d2..5eefffae9f 100644 --- a/src/calibre/manual/creating_plugins.rst +++ b/src/calibre/manual/creating_plugins.rst @@ -142,7 +142,10 @@ Getting resources from the plugin zip file Enabling user configuration of your plugin ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -To allow users to configure your plugin, you must define a couple of methods in your base plugin class, **config_widget** and **save_settings** as shown below: +To allow users to configure your plugin, you must define three methods in your base plugin class, '**is_customizable**, **config_widget** and **save_settings** as shown below: + +.. literalinclude:: plugin_examples/interface_demo/__init__.py + :pyobject: InterfacePluginDemo.is_customizable .. literalinclude:: plugin_examples/interface_demo/__init__.py :pyobject: InterfacePluginDemo.config_widget diff --git a/src/calibre/manual/plugin_examples/interface_demo/__init__.py b/src/calibre/manual/plugin_examples/interface_demo/__init__.py index 2ad4a0245e..9e13879fe9 100644 --- a/src/calibre/manual/plugin_examples/interface_demo/__init__.py +++ b/src/calibre/manual/plugin_examples/interface_demo/__init__.py @@ -32,6 +32,13 @@ class InterfacePluginDemo(InterfaceActionBase): #: The specified class must be defined in the specified module. actual_plugin = 'calibre_plugins.interface_demo.ui:InterfacePlugin' + def is_customizable(self): + ''' + This method must return True to enable customization via + Preferences->Plugins + ''' + return True + def config_widget(self): ''' Implement this method and :meth:`save_settings` in your plugin to From ed1e75cc822ce160ff77c543df9c011a9c7c8ede Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 27 Mar 2011 17:13:56 -0600 Subject: [PATCH 15/37] Fix adding the same plugin twice to a running calibre would not cause a code reload. --- src/calibre/customize/zipplugin.py | 21 ++++++++++++------- .../plugin_examples/helloworld/__init__.py | 2 +- .../interface_demo/__init__.py | 6 +++++- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/calibre/customize/zipplugin.py b/src/calibre/customize/zipplugin.py index 7f23cf46e2..ca07462b9c 100644 --- a/src/calibre/customize/zipplugin.py +++ b/src/calibre/customize/zipplugin.py @@ -12,6 +12,7 @@ import os, zipfile, posixpath, importlib, threading, re, imp, sys from collections import OrderedDict from functools import partial +from calibre import as_unicode from calibre.customize import (Plugin, numeric_version, platform, InvalidPlugin, PluginNotFound) @@ -160,27 +161,31 @@ class PluginLoader(object): try: ans = None - m = importlib.import_module( - 'calibre_plugins.%s'%plugin_name) + plugin_module = 'calibre_plugins.%s'%plugin_name + m = sys.modules.get(plugin_module, None) + if m is not None: + reload(m) + else: + m = importlib.import_module(plugin_module) for obj in m.__dict__.itervalues(): if isinstance(obj, type) and issubclass(obj, Plugin) and \ obj.name != 'Trivial Plugin': ans = obj break if ans is None: - raise InvalidPlugin('No plugin class found in %r:%r'%( - path_to_zip_file, plugin_name)) + raise InvalidPlugin('No plugin class found in %s:%s'%( + as_unicode(path_to_zip_file), plugin_name)) if ans.minimum_calibre_version > numeric_version: raise InvalidPlugin( - 'The plugin at %r needs a version of calibre >= %r' % - (path_to_zip_file, '.'.join(map(str, + 'The plugin at %s needs a version of calibre >= %s' % + (as_unicode(path_to_zip_file), '.'.join(map(unicode, ans.minimum_calibre_version)))) if platform not in ans.supported_platforms: raise InvalidPlugin( - 'The plugin at %r cannot be used on %s' % - (path_to_zip_file, platform)) + 'The plugin at %s cannot be used on %s' % + (as_unicode(path_to_zip_file), platform)) return ans except: diff --git a/src/calibre/manual/plugin_examples/helloworld/__init__.py b/src/calibre/manual/plugin_examples/helloworld/__init__.py index b9b693d9a9..3a49123e07 100644 --- a/src/calibre/manual/plugin_examples/helloworld/__init__.py +++ b/src/calibre/manual/plugin_examples/helloworld/__init__.py @@ -19,7 +19,7 @@ class HelloWorld(FileTypePlugin): version = (1, 0, 0) # The version number of this plugin file_types = set(['epub', 'mobi']) # The file types that this plugin will be applied to on_postprocess = True # Run this plugin after conversion is complete - minimum_calibre_version = (0, 7, 52) + minimum_calibre_version = (0, 7, 53) def run(self, path_to_ebook): from calibre.ebooks.metadata.meta import get_metadata, set_metadata diff --git a/src/calibre/manual/plugin_examples/interface_demo/__init__.py b/src/calibre/manual/plugin_examples/interface_demo/__init__.py index 9e13879fe9..336e498a14 100644 --- a/src/calibre/manual/plugin_examples/interface_demo/__init__.py +++ b/src/calibre/manual/plugin_examples/interface_demo/__init__.py @@ -25,7 +25,7 @@ class InterfacePluginDemo(InterfaceActionBase): supported_platforms = ['windows', 'osx', 'linux'] author = 'Kovid Goyal' version = (1, 0, 0) - minimum_calibre_version = (0, 7, 52) + minimum_calibre_version = (0, 7, 53) #: This field defines the GUI plugin class that contains all the code #: that actually does something. Its format is module_path:class_name @@ -57,6 +57,10 @@ class InterfacePluginDemo(InterfaceActionBase): The base class implementation of this method raises NotImplementedError so by default no user configuration is possible. ''' + # It is important to put this import statement here rather than at the + # top of the module as importing the config class will also cause the + # GUI libraries to be loaded, which we do not want when using calibre + # from the command line from calibre_plugins.interface_demo.config import ConfigWidget return ConfigWidget() From a43e0b644eac16c29f8749b8c9a6151c3e38f04c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 27 Mar 2011 17:34:31 -0600 Subject: [PATCH 16/37] Updated Estadao and Folha de Sao Paulo --- recipes/estadao.recipe | 163 ++++++++++++++++++++-------- recipes/folhadesaopaulo.recipe | 189 +++++++++++++++++++++++---------- 2 files changed, 249 insertions(+), 103 deletions(-) diff --git a/recipes/estadao.recipe b/recipes/estadao.recipe index 4e520c1135..86ab572398 100644 --- a/recipes/estadao.recipe +++ b/recipes/estadao.recipe @@ -1,63 +1,134 @@ #!/usr/bin/env python - -__license__ = 'GPL v3' -__copyright__ = '2010, elsuave' -''' -estadao.com.br -''' - from calibre.web.feeds.news import BasicNewsRecipe +from datetime import datetime, timedelta +from calibre.ebooks.BeautifulSoup import Tag,BeautifulSoup +from calibre.utils.magick import Image, PixelWand +from urllib2 import Request, urlopen, URLError class Estadao(BasicNewsRecipe): - title = 'O Estado de S. Paulo' - __author__ = 'elsuave (modified from Darko Miletic)' - description = 'News from Brasil in Portuguese' - publisher = 'O Estado de S. Paulo' - category = 'news, politics, Brasil' - oldest_article = 2 - max_articles_per_feed = 25 + THUMBALIZR_API = "0123456789abcdef01234567890" # ---->Get your at http://www.thumbalizr.com/ + LANGUAGE = 'pt_br' + language = 'pt' + LANGHTM = 'pt-br' + ENCODING = 'utf' + ENCHTM = 'utf-8' + directionhtm = 'ltr' + requires_version = (0,8,47) + news = True + publication_type = 'newsportal' + + title = u'Estadao' + __author__ = 'Euler Alves' + description = u'Brazilian news from Estad\xe3o' + publisher = u'Estad\xe3o' + category = 'news, rss' + + oldest_article = 4 + max_articles_per_feed = 100 + summary_length = 1000 + + remove_javascript = True no_stylesheets = True use_embedded_content = False - encoding = 'utf8' - cover_url = 'http://www.estadao.com.br/img/logo_estadao.png' - remove_javascript = True + remove_empty_feeds = True + timefmt = ' [%d %b %Y (%a)]' - html2lrf_options = [ - '--comment', description - , '--category', category - , '--publisher', publisher - ] + html2lrf_options = [ + '--comment', description + ,'--category', category + ,'--publisher', publisher + ] - html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"' + html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"' - keep_only_tags = [ - dict(name='div', attrs={'class':['bb-md-noticia','c5']}) - ] + hoje = datetime.now()-timedelta(days=2) + pubdate = hoje.strftime('%a, %d %b') + if hoje.hour<10: + hoje = hoje-timedelta(days=1) + CAPA = 'http://www.estadao.com.br/estadaodehoje/'+hoje.strftime('%Y%m%d')+'/img/capadodia.jpg' + SCREENSHOT = 'http://estadao.com.br/' + cover_margins = (0,0,'white') + masthead_url = 'http://www.estadao.com.br/estadao/novo/img/logo.png' + keep_only_tags = [dict(name='div', attrs={'class':['bb-md-noticia','corpo']})] remove_tags = [ - dict(name=['script','object','form','ul']) - ,dict(name='div', attrs={'class':['fnt2 Color_04 bold','right fnt2 innerTop15 dvTmFont','™_01 right outerLeft15','tituloBox','tags']}) - ,dict(name='div', attrs={'id':['bb-md-noticia-subcom']}) - ] + dict(name='div', + attrs={'id':[ + 'bb-md-noticia-tabs' + ]}) + ,dict(name='div', + attrs={'class':[ + 'tags' + ,'discussion' + ,'bb-gg adsense_container' + ]}) + + ,dict(name='a') + ,dict(name='iframe') + ,dict(name='link') + ,dict(name='script') + ] feeds = [ - (u'Manchetes Estadao', u'http://www.estadao.com.br/rss/manchetes.xml') - ,(u'Ultimas noticias', u'http://www.estadao.com.br/rss/ultimas.xml') - ,(u'Nacional', u'http://www.estadao.com.br/rss/nacional.xml') - ,(u'Internacional', u'http://www.estadao.com.br/rss/internacional.xml') - ,(u'Cidades', u'http://www.estadao.com.br/rss/cidades.xml') - ,(u'Esportes', u'http://www.estadao.com.br/rss/esportes.xml') - ,(u'Arte & Lazer', u'http://www.estadao.com.br/rss/arteelazer.xml') - ,(u'Economia', u'http://www.estadao.com.br/rss/economia.xml') - ,(u'Vida &', u'http://www.estadao.com.br/rss/vidae.xml') - ] + (u'\xDAltimas Not\xEDcias', u'http://www.estadao.com.br/rss/ultimas.xml') + ,(u'Manchetes', u'http://www.estadao.com.br/rss/manchetes.xml') + ,(u'Brasil', u'http://www.estadao.com.br/rss/brasil.xml') + ,(u'Internacional', u'http://www.estadao.com.br/rss/internacional.xml') + ,(u'Cinema', u'http://blogs.estadao.com.br/cinema/feed/') + ,(u'Planeta', u'http://www.estadao.com.br/rss/planeta.xml') + ,(u'Ci\xEAncia', u'http://www.estadao.com.br/rss/ciencia.xml') + ,(u'Sa\xFAde', u'http://www.estadao.com.br/rss/saude.xml') + ,(u'Pol\xEDtica', u'http://www.estadao.com.br/rss/politica.xml') + ] + conversion_options = { + 'title' : title + ,'comments' : description + ,'publisher' : publisher + ,'tags' : category + ,'language' : LANGUAGE + ,'linearize_tables': True + } + def preprocess_html(self, soup): + for item in soup.findAll(style=True): + del item['style'] + if not soup.find(attrs={'http-equiv':'Content-Language'}): + meta0 = Tag(soup,'meta',[("http-equiv","Content-Language"),("content",self.LANGHTM)]) + soup.head.insert(0,meta0) + if not soup.find(attrs={'http-equiv':'Content-Type'}): + meta1 = Tag(soup,'meta',[("http-equiv","Content-Type"),("content","text/html; charset="+self.ENCHTM)]) + soup.head.insert(0,meta1) + return soup - language = 'pt' - - def get_article_url(self, article): - url = BasicNewsRecipe.get_article_url(self, article) - if '/Multimidia/' not in url: - return url + def postprocess_html(self, soup, first): + #process all the images. assumes that the new html has the correct path + for tag in soup.findAll(lambda tag: tag.name.lower()=='img' and tag.has_key('src')): + iurl = tag['src'] + img = Image() + img.open(iurl) + width, height = img.size + print 'img is: ', iurl, 'width is: ', width, 'height is: ', height + pw = PixelWand() + if( width > height and width > 590) : + print 'Rotate image' + img.rotate(pw, -90) + img.save(iurl) + return soup + def get_cover_url(self): + cover_url = self.CAPA + pedido = Request(self.CAPA) + pedido.add_header('User-agent','Mozilla/5.0 (Windows; U; Windows NT 5.1; '+self.LANGHTM+'; userid='+self.THUMBALIZR_API+') Calibre/0.8.47 (like Gecko)') + pedido.add_header('Accept-Charset',self.ENCHTM) + pedido.add_header('Referer',self.SCREENSHOT) + try: + resposta = urlopen(pedido) + soup = BeautifulSoup(resposta) + cover_item = soup.find('body') + if cover_item: + cover_url='http://api.thumbalizr.com/?api_key='+self.THUMBALIZR_API+'&url='+self.SCREENSHOT+'&width=600&quality=90' + return cover_url + except URLError: + cover_url='http://api.thumbalizr.com/?api_key='+self.THUMBALIZR_API+'&url='+self.SCREENSHOT+'&width=600&quality=90' + return cover_url diff --git a/recipes/folhadesaopaulo.recipe b/recipes/folhadesaopaulo.recipe index 262a265020..40898672e6 100644 --- a/recipes/folhadesaopaulo.recipe +++ b/recipes/folhadesaopaulo.recipe @@ -1,74 +1,149 @@ -#!/usr/bin/env python - -__license__ = 'GPL v3' -__copyright__ = '2010, Saverio Palmieri Neto ' -''' -folha.uol.com.br -''' - from calibre.web.feeds.news import BasicNewsRecipe +from datetime import datetime, timedelta +from calibre.ebooks.BeautifulSoup import Tag,BeautifulSoup +from calibre.utils.magick import Image, PixelWand +from urllib2 import Request, urlopen, URLError class FolhaOnline(BasicNewsRecipe): - title = 'Folha de Sao Paulo' - __author__ = 'Saverio Palmieri Neto' - description = 'Brazilian news from Folha de Sao Paulo Online' - publisher = 'Folha de Sao Paulo' - category = 'Brasil, news' - oldest_article = 2 - max_articles_per_feed = 1000 - summary_length = 2048 + THUMBALIZR_API = "0123456789abcdef01234567890" # ---->Get your at http://www.thumbalizr.com/ + LANGUAGE = 'pt_br' + language = 'pt' + LANGHTM = 'pt-br' + ENCODING = 'cp1252' + ENCHTM = 'iso-8859-1' + directionhtm = 'ltr' + requires_version = (0,8,47) + news = True + publication_type = 'newsportal' + + title = u'Folha de S\xE3o Paulo' + __author__ = 'Euler Alves' + description = u'Brazilian news from Folha de S\xE3o Paulo' + publisher = u'Folha de S\xE3o Paulo' + category = 'news, rss' + + oldest_article = 4 + max_articles_per_feed = 100 + summary_length = 1000 + + remove_javascript = True no_stylesheets = True use_embedded_content = False + remove_empty_feeds = True timefmt = ' [%d %b %Y (%a)]' - encoding = 'cp1252' - cover_url = 'http://lh5.ggpht.com/_hEb7sFmuBvk/TFoiKLRS5dI/AAAAAAAAADM/kcVKggZwKnw/capa_folha.jpg' - cover_margins = (5,5,'white') - remove_javascript = True - keep_only_tags = [dict(name='div', attrs={'id':'articleNew'})] + html2lrf_options = [ + '--comment', description + ,'--category', category + ,'--publisher', publisher + ] - remove_tags = [ - dict(name='script') - ,dict(name='div', - attrs={'id':[ - 'articleButton' - ,'bookmarklets' - ,'ad-180x150-1' - ,'contextualAdsArticle' - ,'articleEnd' - ,'articleComments' - ]}) - ,dict(name='div', - attrs={'class':[ - 'openBox adslibraryArticle' - ]}) - ,dict(name='a') - ,dict(name='iframe') - ,dict(name='link') - ] + html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"' + hoje = datetime.now() + pubdate = hoje.strftime('%a, %d %b') + if hoje.hour<6: + hoje = hoje-timedelta(days=1) + CAPA = 'http://www1.folha.uol.com.br/fsp/images/cp'+hoje.strftime('%d%m%Y')+'.jpg' + SCREENSHOT = 'http://www1.folha.uol.com.br/' + cover_margins = (0,0,'white') + masthead_url = 'http://f.i.uol.com.br/fsp/furniture/images/lgo-fsp-430x50-ffffff.gif' + + keep_only_tags = [dict(name='div', attrs={'id':'articleNew'})] + remove_tags = [ + dict(name='div', + attrs={'id':[ + 'articleButton' + ,'bookmarklets' + ,'ad-180x150-1' + ,'contextualAdsArticle' + ,'articleEnd' + ,'articleComments' + ]}) + ,dict(name='div', + attrs={'class':[ + 'openBox adslibraryArticle' + ]}) + + ,dict(name='a') + ,dict(name='iframe') + ,dict(name='link') + ,dict(name='script') + ] feeds = [ - (u'Em cima da hora', u'http://feeds.folha.uol.com.br/emcimadahora/rss091.xml') - ,(u'Ambiente', u'http://feeds.folha.uol.com.br/ambiente/rss091.xml') - ,(u'Bichos', u'http://feeds.folha.uol.com.br/bichos/rss091.xml') - ,(u'Poder', u'http://feeds.folha.uol.com.br/poder/rss091.xml') - ,(u'Ciencia', u'http://feeds.folha.uol.com.br/ciencia/rss091.xml') - ,(u'Cotidiano', u'http://feeds.folha.uol.com.br/cotidiado/rss091.xml') - ,(u'Saber', u'http://feeds.folha.uol.com.br/saber/rss091.xml') - ,(u'Equilíbrio e Saúde', u'http://feeds.folha.uol.com.br/equilibrioesaude/rss091.xml') - ,(u'Esporte', u'http://feeds.folha.uol.com.br/esporte/rss091.xml') - ,(u'Ilustrada', u'http://feeds.folha.uol.com.br/ilustrada/rss091.xml') - ,(u'Ilustríssima', u'http://feeds.folha.uol.com.br/ilustrissima/rss091.xml') - ,(u'Mercado', u'http://feeds.folha.uol.com.br/mercado/rss091.xml') - ,(u'Mundo', u'http://feeds.folha.uol.com.br/mundo/rss091.xml') - ,(u'Tec', u'http://feeds.folha.uol.com.br/tec/rss091.xml') - ,(u'Turismo', u'http://feeds.folha.uol.com.br/turismo/rss091.xml') - ] + (u'Em cima da hora', u'http://feeds.folha.uol.com.br/emcimadahora/rss091.xml') + ,(u'Ambiente', u'http://feeds.folha.uol.com.br/ambiente/rss091.xml') + ,(u'Bichos', u'http://feeds.folha.uol.com.br/bichos/rss091.xml') + ,(u'Ci\xEAncia', u'http://feeds.folha.uol.com.br/ciencia/rss091.xml') + ,(u'Poder', u'http://feeds.folha.uol.com.br/poder/rss091.xml') + ,(u'Equil\xEDbrio e Sa\xFAde', u'http://feeds.folha.uol.com.br/equilibrioesaude/rss091.xml') + ,(u'Turismo', u'http://feeds.folha.uol.com.br/folha/turismo/rss091.xml') + ,(u'Mundo', u'http://feeds.folha.uol.com.br/mundo/rss091.xml') + ,(u'Pelo Mundo', u'http://feeds.folha.uol.com.br/pelomundo.folha.rssblog.uol.com.br/') + ,(u'Circuito integrado', u'http://feeds.folha.uol.com.br/circuitointegrado.folha.rssblog.uol.com.br/') + ,(u'Blog do Fred', u'http://feeds.folha.uol.com.br/blogdofred.folha.rssblog.uol.com.br/') + ,(u'Maria In\xEAs Dolci', u'http://feeds.folha.uol.com.br/mariainesdolci.folha.blog.uol.com.br/') + ,(u'Eduardo Ohata', u'http://feeds.folha.uol.com.br/folha/pensata/eduardoohata/rss091.xml') + ,(u'Kennedy Alencar', u'http://feeds.folha.uol.com.br/folha/pensata/kennedyalencar/rss091.xml') + ,(u'Eliane Catanh\xEAde', u'http://feeds.folha.uol.com.br/folha/pensata/elianecantanhede/rss091.xml') + ,(u'Fernado Canzian', u'http://feeds.folha.uol.com.br/folha/pensata/fernandocanzian/rss091.xml') + ,(u'Gilberto Dimenstein', u'http://feeds.folha.uol.com.br/folha/pensata/gilbertodimenstein/rss091.xml') + ,(u'H\xE9lio Schwartsman', u'http://feeds.folha.uol.com.br/folha/pensata/helioschwartsman/rss091.xml') + ,(u'Jo\xE3o Pereira Coutinho', u'http://http://feeds.folha.uol.com.br/folha/pensata/joaopereiracoutinho/rss091.xml') + ,(u'Luiz Caversan', u'http://http://feeds.folha.uol.com.br/folha/pensata/luizcaversan/rss091.xml') + ,(u'S\xE9rgio Malbergier', u'http://http://feeds.folha.uol.com.br/folha/pensata/sergiomalbergier/rss091.xml') + ,(u'Valdo Cruz', u'http://http://feeds.folha.uol.com.br/folha/pensata/valdocruz/rss091.xml') + ] + + conversion_options = { + 'title' : title + ,'comments' : description + ,'publisher' : publisher + ,'tags' : category + ,'language' : LANGUAGE + ,'linearize_tables': True + } def preprocess_html(self, soup): for item in soup.findAll(style=True): del item['style'] + if not soup.find(attrs={'http-equiv':'Content-Language'}): + meta0 = Tag(soup,'meta',[("http-equiv","Content-Language"),("content",self.LANGHTM)]) + soup.head.insert(0,meta0) + if not soup.find(attrs={'http-equiv':'Content-Type'}): + meta1 = Tag(soup,'meta',[("http-equiv","Content-Type"),("content","text/html; charset="+self.ENCHTM)]) + soup.head.insert(0,meta1) return soup - language = 'pt' + def postprocess_html(self, soup, first): + #process all the images. assumes that the new html has the correct path + for tag in soup.findAll(lambda tag: tag.name.lower()=='img' and tag.has_key('src')): + iurl = tag['src'] + img = Image() + img.open(iurl) + width, height = img.size + print 'img is: ', iurl, 'width is: ', width, 'height is: ', height + pw = PixelWand() + if( width > height and width > 590) : + print 'Rotate image' + img.rotate(pw, -90) + img.save(iurl) + return soup + + def get_cover_url(self): + cover_url = self.CAPA + pedido = Request(self.CAPA) + pedido.add_header('User-agent','Mozilla/5.0 (Windows; U; Windows NT 5.1; '+self.LANGHTM+'; userid='+self.THUMBALIZR_API+') Calibre/0.8.47 (like Gecko)') + pedido.add_header('Accept-Charset',self.ENCHTM) + pedido.add_header('Referer',self.SCREENSHOT) + try: + resposta = urlopen(pedido) + soup = BeautifulSoup(resposta) + cover_item = soup.find('body') + if cover_item: + cover_url='http://api.thumbalizr.com/?api_key='+self.THUMBALIZR_API+'&url='+self.SCREENSHOT+'&width=600&quality=90' + return cover_url + except URLError: + cover_url='http://api.thumbalizr.com/?api_key='+self.THUMBALIZR_API+'&url='+self.SCREENSHOT+'&width=600&quality=90' + return cover_url From 9d54c7b3b2b08ebb0c21970ce344e47f6fabc9bd Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 27 Mar 2011 17:39:29 -0600 Subject: [PATCH 17/37] ... --- src/calibre/customize/__init__.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/calibre/customize/__init__.py b/src/calibre/customize/__init__.py index faa6fcada2..891e0e0772 100644 --- a/src/calibre/customize/__init__.py +++ b/src/calibre/customize/__init__.py @@ -529,9 +529,13 @@ class InterfaceActionBase(Plugin): # {{{ ''' This method must return the actual interface action plugin object. ''' - mod, cls = self.actual_plugin.split(':') - return getattr(importlib.import_module(mod), cls)(gui, - self.site_customization) + ac = getattr(self, 'actual_plugin_', None) + if ac is None: + mod, cls = self.actual_plugin.split(':') + ac = getattr(importlib.import_module(mod), cls)(gui, + self.site_customization) + self.actual_plugin_ = ac + return ac # }}} From d1289664b086c10e75a4b2c98d689952abc925db Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 27 Mar 2011 17:41:16 -0600 Subject: [PATCH 18/37] ... --- src/calibre/customize/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/calibre/customize/__init__.py b/src/calibre/customize/__init__.py index 891e0e0772..816c275f72 100644 --- a/src/calibre/customize/__init__.py +++ b/src/calibre/customize/__init__.py @@ -525,11 +525,15 @@ class InterfaceActionBase(Plugin): # {{{ actual_plugin = None + def __init__(self, *args, **kwargs): + Plugin.__init__(self, *args, **kwargs) + self.actual_plugin_ = None + def load_actual_plugin(self, gui): ''' This method must return the actual interface action plugin object. ''' - ac = getattr(self, 'actual_plugin_', None) + ac = self.actual_plugin_ if ac is None: mod, cls = self.actual_plugin.split(':') ac = getattr(importlib.import_module(mod), cls)(gui, From e47e410b4e93076e3a7834db83d5521fac5c3ce7 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 27 Mar 2011 17:47:20 -0600 Subject: [PATCH 19/37] ... --- .../manual/plugin_examples/interface_demo/__init__.py | 5 +++++ src/calibre/manual/plugin_examples/interface_demo/ui.py | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/calibre/manual/plugin_examples/interface_demo/__init__.py b/src/calibre/manual/plugin_examples/interface_demo/__init__.py index 336e498a14..ac7d9c6ec1 100644 --- a/src/calibre/manual/plugin_examples/interface_demo/__init__.py +++ b/src/calibre/manual/plugin_examples/interface_demo/__init__.py @@ -72,4 +72,9 @@ class InterfacePluginDemo(InterfaceActionBase): ''' config_widget.save_settings() + # Apply the changes + ac = self.actual_plugin_ + if ac is not None: + ac.apply_settings() + diff --git a/src/calibre/manual/plugin_examples/interface_demo/ui.py b/src/calibre/manual/plugin_examples/interface_demo/ui.py index 9b60a66ed4..5026269b60 100644 --- a/src/calibre/manual/plugin_examples/interface_demo/ui.py +++ b/src/calibre/manual/plugin_examples/interface_demo/ui.py @@ -17,7 +17,6 @@ if False: from calibre.gui2.actions import InterfaceAction from calibre_plugins.interface_demo.main import DemoDialog - class InterfacePlugin(InterfaceAction): name = 'Interface Plugin Demo' @@ -64,4 +63,9 @@ class InterfacePlugin(InterfaceAction): d = DemoDialog(self.gui, self.qaction.icon(), do_user_config) d.show() + def apply_settings(self): + from calibre_plugins.interface_demo.config import prefs + # In an actual non trivial plugin, you would probably need to + # do something based on the settings in prefs + prefs From 9f6a6defad54fa248db5b5bc8088ad66573324a7 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 27 Mar 2011 18:00:10 -0600 Subject: [PATCH 20/37] ... --- src/calibre/manual/creating_plugins.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/calibre/manual/creating_plugins.rst b/src/calibre/manual/creating_plugins.rst index 5eefffae9f..dd62e4e416 100644 --- a/src/calibre/manual/creating_plugins.rst +++ b/src/calibre/manual/creating_plugins.rst @@ -49,7 +49,8 @@ A User Interface plugin ------------------------- This plugin will be spread over a few files (to keep the code clean). It will show you how to get resources -(images or data files) from the plugin zip file, how to create elements in the |app| user interface and how to access +(images or data files) from the plugin zip file, allow users to configure your plugin, +how to create elements in the |app| user interface and how to access and query the books database in |app|. You can download this plugin from `interface_demo_plugin.zip `_ From cec36461bd468ebafbd6474d97b15a6c7598c2ad Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 27 Mar 2011 20:00:02 -0600 Subject: [PATCH 21/37] Fix #743701 (Untranslated texts) --- src/calibre/gui2/preferences/main.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/calibre/gui2/preferences/main.py b/src/calibre/gui2/preferences/main.py index 699087c4bb..d930fb3ebd 100644 --- a/src/calibre/gui2/preferences/main.py +++ b/src/calibre/gui2/preferences/main.py @@ -71,11 +71,11 @@ class Category(QWidget): # {{{ plugin_activated = pyqtSignal(object) - def __init__(self, name, plugins, parent=None): + def __init__(self, name, plugins, gui_name, parent=None): QWidget.__init__(self, parent) self._layout = QVBoxLayout() self.setLayout(self._layout) - self.label = QLabel(name) + self.label = QLabel(gui_name) self.sep = QFrame(self) self.bf = QFont() self.bf.setBold(True) @@ -118,12 +118,17 @@ class Browser(QScrollArea): # {{{ QScrollArea.__init__(self, parent) self.setWidgetResizable(True) - category_map = {} + category_map, category_names = {}, {} for plugin in preferences_plugins(): if plugin.category not in category_map: category_map[plugin.category] = plugin.category_order if category_map[plugin.category] < plugin.category_order: category_map[plugin.category] = plugin.category_order + if plugin.category not in category_names: + category_names[plugin.category] = (plugin.gui_category if + plugin.gui_category else plugin.category) + + self.category_names = category_names categories = list(category_map.keys()) categories.sort(cmp=lambda x, y: cmp(category_map[x], category_map[y])) @@ -145,7 +150,7 @@ class Browser(QScrollArea): # {{{ self.setWidget(self.container) for name, plugins in self.category_map.items(): - w = Category(name, plugins, self) + w = Category(name, plugins, self.category_names[name], parent=self) self.widgets.append(w) self._layout.addWidget(w) w.plugin_activated.connect(self.show_plugin.emit) From bea32d35619c2bfbdf7a5fd992f8498ff773c67d Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 27 Mar 2011 20:03:00 -0600 Subject: [PATCH 22/37] ... --- src/calibre/customize/builtins.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 994d73d33b..7d21f8255e 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -853,7 +853,7 @@ class Columns(PreferencesPlugin): class Toolbar(PreferencesPlugin): name = 'Toolbar' icon = I('wizard.png') - gui_name = _('Customize the toolbar') + gui_name = _('Toolbar') category = 'Interface' gui_category = _('Interface') category_order = 1 @@ -865,7 +865,7 @@ class Toolbar(PreferencesPlugin): class Search(PreferencesPlugin): name = 'Search' icon = I('search.png') - gui_name = _('Customize searching') + gui_name = _('Searching') category = 'Interface' gui_category = _('Interface') category_order = 1 From 9b0ecef5da6dd9c14540c820e2523c645a963acf Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 27 Mar 2011 20:08:45 -0600 Subject: [PATCH 23/37] Fix #743599 (Advent Vega Android device not recognised) --- src/calibre/devices/android/driver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py index 39f24ff6ee..54e4979524 100644 --- a/src/calibre/devices/android/driver.py +++ b/src/calibre/devices/android/driver.py @@ -94,14 +94,14 @@ class ANDROID(USBMS): VENDOR_NAME = ['HTC', 'MOTOROLA', 'GOOGLE_', 'ANDROID', 'ACER', 'GT-I5700', 'SAMSUNG', 'DELL', 'LINUX', 'GOOGLE', 'ARCHOS', - 'TELECHIP', 'HUAWEI', 'T-MOBILE', 'SEMC', 'LGE'] + 'TELECHIP', 'HUAWEI', 'T-MOBILE', 'SEMC', 'LGE', 'NVIDIA'] WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE', '__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', 'SGH-I897', 'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'SCH-I500_CARD', 'SPH-D700_CARD', 'MB810', 'GT-P1000', 'DESIRE', 'SGH-T849', '_MB300', 'A70S', 'S_ANDROID', 'A101IT', 'A70H', 'IDEOS_TABLET', 'MYTOUCH_4G', 'UMS_COMPOSITE', 'SCH-I800_CARD', - '7', 'A956', 'A955', 'A43', 'ANDROID_PLATFORM'] + '7', 'A956', 'A955', 'A43', 'ANDROID_PLATFORM', 'TEGRA_2'] WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD', 'A70S', 'A101IT', '7'] From df9c6fe7677c9f585e7623f78ed607eb25c602f2 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 27 Mar 2011 20:18:52 -0600 Subject: [PATCH 24/37] Add a tweak that can be used to have the calibre content server listen for IPv6 connections. Fixes #743486 (library/server/ doesn't listen on ipv6) --- resources/default_tweaks.py | 8 ++++++++ src/calibre/library/server/__init__.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/resources/default_tweaks.py b/resources/default_tweaks.py index 464c9d2cfd..1cf699efa3 100644 --- a/resources/default_tweaks.py +++ b/resources/default_tweaks.py @@ -363,3 +363,11 @@ maximum_cover_size = (1200, 1600) # the files will be sent to the location with the most free space. send_news_to_device_location = "main" +#: What interfaces should the content server listen on +# By default, the calibre content server listens on '0.0.0.0' which means that it +# accepts IPv4 connections on all interfaces. You can change this to, for +# example, '127.0.0.1' to only listen for connections from the local machine, or +# to '::' to listen to all incoming IPv6 and IPv4 connections (this may not +# work on all operating systems) +server_listen_on = '0.0.0.0' + diff --git a/src/calibre/library/server/__init__.py b/src/calibre/library/server/__init__.py index 244669f50a..950c881d8d 100644 --- a/src/calibre/library/server/__init__.py +++ b/src/calibre/library/server/__init__.py @@ -10,7 +10,7 @@ import os from calibre.utils.config import Config, StringConfig, config_dir, tweaks -listen_on = '0.0.0.0' +listen_on = tweaks['server_listen_on'] log_access_file = os.path.join(config_dir, 'server_access_log.txt') From 8f18065d3c484f8b9765ee084fa5f46243c3cf0f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 27 Mar 2011 21:06:16 -0600 Subject: [PATCH 25/37] RTF Input: Handle RTF files with too many levels of list nesting. Fixes #743243 (IndexError: list index out of range Converting RTF to EPUB, LRF) --- src/calibre/ebooks/rtf2xml/make_lists.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/calibre/ebooks/rtf2xml/make_lists.py b/src/calibre/ebooks/rtf2xml/make_lists.py index fc544ac499..b785bf85d2 100755 --- a/src/calibre/ebooks/rtf2xml/make_lists.py +++ b/src/calibre/ebooks/rtf2xml/make_lists.py @@ -291,9 +291,12 @@ class MakeLists: if self.__list_of_lists: # older RTF won't generate a list_of_lists index_of_list = self.__get_index_of_list(id) if index_of_list != None:# found a matching id - list_dict = self.__list_of_lists[index_of_list][0] + curlist = self.__list_of_lists[index_of_list] + list_dict = curlist[0] level = int(self.__level) + 1 - level_dict = self.__list_of_lists[index_of_list][level][0] + if level >= len(curlist): + level = len(curlist) - 1 + level_dict = curlist[level][0] list_type = level_dict.get('numbering-type') if list_type == 'bullet': list_type = 'unordered' From e6cb34d6009ceca26c6f76b39823bbeb079af0f8 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 27 Mar 2011 23:58:17 -0600 Subject: [PATCH 26/37] ... --- recipes/wash_post.recipe | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/recipes/wash_post.recipe b/recipes/wash_post.recipe index 3af89d502e..0ca6410b4e 100644 --- a/recipes/wash_post.recipe +++ b/recipes/wash_post.recipe @@ -39,14 +39,26 @@ class WashingtonPost(BasicNewsRecipe): {'class':lambda x: x and 'also-read' in x.split()}, {'class':lambda x: x and 'partners-content' in x.split()}, {'class':['module share', 'module ads', 'comment-vars', 'hidden', - 'share-icons-wrap', 'comments']}, - {'id':['right-rail']}, + 'share-icons-wrap', 'comments', 'flipper']}, + {'id':['right-rail', 'save-and-share']}, + {'width':'1', 'height':'1'}, + + ] - ] keep_only_tags = dict(id=['content', 'article']) + def get_article_url(self, *args): + ans = BasicNewsRecipe.get_article_url(self, *args) + ans = ans.rpartition('?')[0] + if ans.endswith('_video.html'): + return None + if 'ads.pheedo.com' in ans: + return None + #if not ans.endswith('_blog.html'): + # return None + return ans + def print_version(self, url): - url = url.rpartition('?')[0] return url.replace('_story.html', '_singlePage.html') From 517a8d82cd0cffcff1d96123633e8a79e6b94e74 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 28 Mar 2011 08:46:21 -0600 Subject: [PATCH 27/37] EPUB Input: Workaround for invalid EPUBs produced by someone named 'ibooks, Inc.'. Fixes #744122 (Conversion of epub failed) --- src/calibre/ebooks/oeb/reader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/ebooks/oeb/reader.py b/src/calibre/ebooks/oeb/reader.py index 4a09e0b1d4..ebc2f30d00 100644 --- a/src/calibre/ebooks/oeb/reader.py +++ b/src/calibre/ebooks/oeb/reader.py @@ -103,8 +103,8 @@ class OEBReader(object): data = self.oeb.container.read(None) data = self.oeb.decode(data) data = XMLDECL_RE.sub('', data) - data = data.replace('http://openebook.org/namespaces/oeb-package/1.0', - OPF1_NS) + data = re.sub(r'http://openebook.org/namespaces/oeb-package/1.0(/*)', + OPF1_NS, data) try: opf = etree.fromstring(data) except etree.XMLSyntaxError: From 28a5223eedf1eec1cf0ce577b8260ab9b398bd80 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 28 Mar 2011 09:04:59 -0600 Subject: [PATCH 28/37] Skeleton code for Open feedback in Apple driver --- src/calibre/devices/apple/driver.py | 31 ++++++++++++++++++++++------- src/calibre/devices/errors.py | 7 +++++++ src/calibre/gui2/device.py | 9 ++++++--- 3 files changed, 37 insertions(+), 10 deletions(-) diff --git a/src/calibre/devices/apple/driver.py b/src/calibre/devices/apple/driver.py index 0cfa8f1ab2..4174de3ac7 100644 --- a/src/calibre/devices/apple/driver.py +++ b/src/calibre/devices/apple/driver.py @@ -24,6 +24,29 @@ from calibre.utils.logging import Log from calibre.utils.zipfile import ZipFile + +class AppleOpenFeedback(OpenFeedback): + + def __init__(self): + OpenFeedback.__init__(self, u'') + + def custom_dialog(self, parent): + from PyQt4.Qt import (QDialog, QVBoxLayout, QLabel, QDialogButtonBox) + + class Dialog(QDialog): + + def __init__(self, p): + QDialog.__init__(self, p) + self.l = l = QVBoxLayout() + self.setLayout(l) + l.addWidget(QLabel('test')) + self.bb = QDialogButtonBox(QDialogButtonBox.OK) + l.addWidget(self.bb) + self.bb.accepted.connect(self.accept) + self.bb.rejected.connect(self.reject) + + return Dialog(parent) + from PIL import Image as PILImage from lxml import etree @@ -744,13 +767,7 @@ class ITUNES(DriverBase): # Display a dialog recommending using 'Connect to iTunes' if False and not self.settings().extra_customization[self.SKIP_CONNECT_TO_ITUNES_DIALOG]: - raise OpenFeedback('

' + ('Click the "Connect/Share" button and choose' - ' "Connect to iTunes" to send books from your calibre library' - ' to your Apple iDevice.

For more information, see ' - '' - 'Calibre + Apple iDevices FAQ.

' - 'After following the Quick Start steps outlined in the FAQ, ' - 'restart calibre.')) + raise AppleOpenFeedback() if DEBUG: self.log.info(" advanced user mode, directly connecting to iDevice") diff --git a/src/calibre/devices/errors.py b/src/calibre/devices/errors.py index 3d88eb741f..7b11b6933f 100644 --- a/src/calibre/devices/errors.py +++ b/src/calibre/devices/errors.py @@ -41,6 +41,13 @@ class OpenFeedback(DeviceError): self.feedback_msg = msg DeviceError.__init__(self, msg) + def custom_dialog(self, parent): + ''' + If you need to show the user a custom dialog, instead if just + displaying the feedback_msg, create and return it here. + ''' + raise NotImplementedError + class DeviceBusy(ProtocolError): """ Raised when device is busy """ def __init__(self, uerr=""): diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index ab2177cef1..6163c01d27 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -164,7 +164,7 @@ class DeviceManager(Thread): # {{{ dev.open(self.current_library_uuid) except OpenFeedback as e: if dev not in self.ejected_devices: - self.open_feedback_msg(dev.get_gui_name(), e.feedback_msg) + self.open_feedback_msg(dev.get_gui_name(), e) self.ejected_devices.add(dev) continue except: @@ -618,8 +618,11 @@ class DeviceMixin(object): # {{{ if tweaks['auto_connect_to_folder']: self.connect_to_folder_named(tweaks['auto_connect_to_folder']) - def show_open_feedback(self, devname, msg): - self.__of_dev_mem__ = d = info_dialog(self, devname, msg) + def show_open_feedback(self, devname, e): + try: + self.__of_dev_mem__ = d = e.custom_dialog(self) + except NotImplementedError: + self.__of_dev_mem__ = d = info_dialog(self, devname, e.feedback_msg) d.show() def auto_convert_question(self, msg, autos): From e7b39e91584dede387cf4c23a7c5c8b39d6ca1fe Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 28 Mar 2011 10:14:53 -0600 Subject: [PATCH 29/37] MOBI Output: Workaround for Amazon's MOBI renderer not rendering top margins on ul and ol tags. Fixes 744365 --- src/calibre/ebooks/mobi/mobiml.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/calibre/ebooks/mobi/mobiml.py b/src/calibre/ebooks/mobi/mobiml.py index 189739986d..ffb19d5fce 100644 --- a/src/calibre/ebooks/mobi/mobiml.py +++ b/src/calibre/ebooks/mobi/mobiml.py @@ -206,7 +206,11 @@ class MobiMLizer(object): vspace = bstate.vpadding + bstate.vmargin bstate.vpadding = bstate.vmargin = 0 if tag not in TABLE_TAGS: - wrapper.attrib['height'] = self.mobimlize_measure(vspace) + if tag in ('ul', 'ol') and vspace > 0: + wrapper.addprevious(etree.Element(XHTML('div'), + height=self.mobimlize_measure(vspace))) + else: + wrapper.attrib['height'] = self.mobimlize_measure(vspace) para.attrib['width'] = self.mobimlize_measure(indent) elif tag == 'table' and vspace > 0: vspace = int(round(vspace / self.profile.fbase)) From 42529417ca43dceedc7448ae46cb980ffa566c79 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 28 Mar 2011 18:18:14 +0100 Subject: [PATCH 30/37] Ensure that custcol['display'] is never None. --- src/calibre/library/custom_columns.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/calibre/library/custom_columns.py b/src/calibre/library/custom_columns.py index 48960ac871..8eed121b21 100644 --- a/src/calibre/library/custom_columns.py +++ b/src/calibre/library/custom_columns.py @@ -76,6 +76,8 @@ class CustomColumns(object): 'num':record[6], 'is_multiple':record[7], } + if data['display'] is None: + data['display'] = {} table, lt = self.custom_table_names(data['num']) if table not in custom_tables or (data['normalized'] and lt not in custom_tables): From 6958a6f907dc2043b2b9ae9f0f2d6bc887af158d Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 28 Mar 2011 11:20:12 -0600 Subject: [PATCH 31/37] Add warning when MOBI output plugin detects and removes an HTML cover --- src/calibre/ebooks/mobi/mobiml.py | 5 +++++ src/calibre/trac/bzr_commit_plugin.py | 7 +++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/calibre/ebooks/mobi/mobiml.py b/src/calibre/ebooks/mobi/mobiml.py index ffb19d5fce..40ad5e9e78 100644 --- a/src/calibre/ebooks/mobi/mobiml.py +++ b/src/calibre/ebooks/mobi/mobiml.py @@ -102,6 +102,7 @@ class MobiMLizer(object): def __call__(self, oeb, context): oeb.logger.info('Converting XHTML to Mobipocket markup...') self.oeb = oeb + self.log = self.oeb.logger self.opts = context self.profile = profile = context.dest self.fnums = fnums = dict((v, k) for k, v in profile.fnums.items()) @@ -118,6 +119,10 @@ class MobiMLizer(object): del oeb.guide['cover'] item = oeb.manifest.hrefs[href] if item.spine_position is not None: + self.log.warn('Found an HTML cover,', item.href, 'removing it.', + 'If you find some content missing from the output MOBI, it ' + 'is because you misidentified the HTML cover in the input ' + 'document') oeb.spine.remove(item) if item.media_type in OEB_DOCS: self.oeb.manifest.remove(item) diff --git a/src/calibre/trac/bzr_commit_plugin.py b/src/calibre/trac/bzr_commit_plugin.py index 7e5a1367cb..c596425a68 100644 --- a/src/calibre/trac/bzr_commit_plugin.py +++ b/src/calibre/trac/bzr_commit_plugin.py @@ -31,8 +31,11 @@ class cmd_commit(_cmd_commit): summary = '' raw = urllib.urlopen('https://bugs.launchpad.net/calibre/+bug/' + bug).read() - h1 = html.fromstring(raw).xpath('//h1[@id="edit-title"]')[0] - summary = html.tostring(h1, method='text', encoding=unicode).strip() + try: + h1 = html.fromstring(raw).xpath('//h1[@id="edit-title"]')[0] + summary = html.tostring(h1, method='text', encoding=unicode).strip() + except: + summary = 'Private bug' print 'Working on bug:', summary if summary: msg = msg.replace('#%s'%bug, '#%s (%s)'%(bug, summary)) From 3d63ef67ddddb7d45c5c7044354976d0dc493b8f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 28 Mar 2011 11:39:54 -0600 Subject: [PATCH 32/37] Implement #744020 (Add Publisher and Publication Date to Adding Books Possible Fields) --- src/calibre/ebooks/metadata/meta.py | 13 +++++++++ src/calibre/gui2/filename_pattern.ui | 40 ++++++++++++++++++++++++++++ src/calibre/gui2/widgets.py | 6 +++++ 3 files changed, 59 insertions(+) diff --git a/src/calibre/ebooks/metadata/meta.py b/src/calibre/ebooks/metadata/meta.py index cbd9db3f04..b0c43a8182 100644 --- a/src/calibre/ebooks/metadata/meta.py +++ b/src/calibre/ebooks/metadata/meta.py @@ -182,6 +182,19 @@ def metadata_from_filename(name, pat=None): mi.isbn = si except (IndexError, ValueError): pass + try: + publisher = match.group('publisher') + mi.publisher = publisher + except (IndexError, ValueError): + pass + try: + pubdate = match.group('published') + if pubdate: + from calibre.utils.date import parse_date + mi.pubdate = parse_date(pubdate) + except: + pass + if mi.is_null('title'): mi.title = name return mi diff --git a/src/calibre/gui2/filename_pattern.ui b/src/calibre/gui2/filename_pattern.ui index 68b3108e06..c8a9b4f6f6 100644 --- a/src/calibre/gui2/filename_pattern.ui +++ b/src/calibre/gui2/filename_pattern.ui @@ -206,6 +206,46 @@ + + + + Publisher: + + + + + + + Regular expression (?P<publisher>) + + + No match + + + true + + + + + + + Published: + + + + + + + Regular expression (?P<published>) + + + No match + + + true + + + diff --git a/src/calibre/gui2/widgets.py b/src/calibre/gui2/widgets.py index ea0509b51a..e5f1c94342 100644 --- a/src/calibre/gui2/widgets.py +++ b/src/calibre/gui2/widgets.py @@ -121,6 +121,12 @@ class FilenamePattern(QWidget, Ui_Form): else: self.series_index.setText(_('No match')) + if mi.publisher: + self.publisher.setText(mi.publisher) + + if mi.pubdate: + self.pubdate.setText(mi.pubdate.strftime('%Y-%m-%d')) + self.isbn.setText(_('No match') if mi.isbn is None else str(mi.isbn)) From 71d038d975f6605bea97eca555c48ad756a48c69 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 28 Mar 2011 15:29:17 -0600 Subject: [PATCH 33/37] EPUB Output: Remove any margins specified via an Adobe page template in the input document --- src/calibre/ebooks/conversion/plumber.py | 4 +- src/calibre/ebooks/oeb/transforms/margins.py | 56 ------------------- .../ebooks/oeb/transforms/page_margin.py | 20 +++++++ 3 files changed, 23 insertions(+), 57 deletions(-) delete mode 100644 src/calibre/ebooks/oeb/transforms/margins.py diff --git a/src/calibre/ebooks/conversion/plumber.py b/src/calibre/ebooks/conversion/plumber.py index 6272e7b10b..b26befe075 100644 --- a/src/calibre/ebooks/conversion/plumber.py +++ b/src/calibre/ebooks/conversion/plumber.py @@ -1003,8 +1003,10 @@ OptionRecommendation(name='sr3_replace', self.opts.insert_blank_line = oibl self.opts.remove_paragraph_spacing = orps - from calibre.ebooks.oeb.transforms.page_margin import RemoveFakeMargins + from calibre.ebooks.oeb.transforms.page_margin import \ + RemoveFakeMargins, RemoveAdobeMargins RemoveFakeMargins()(self.oeb, self.log, self.opts) + RemoveAdobeMargins()(self.oeb, self.log, self.opts) pr(0.9) self.flush() diff --git a/src/calibre/ebooks/oeb/transforms/margins.py b/src/calibre/ebooks/oeb/transforms/margins.py deleted file mode 100644 index fbdf2e63fd..0000000000 --- a/src/calibre/ebooks/oeb/transforms/margins.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env python -# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai - -__license__ = 'GPL v3' -__copyright__ = '2010, Kovid Goyal ' -__docformat__ = 'restructuredtext en' - - -class RemoveFakeMargins(object): - ''' - Try to detect and remove fake margins inserted by asinine ebook creation - software on each paragraph/wrapper div. Can be used only after CSS - flattening. - ''' - - def __call__(self, oeb, opts, log): - self.oeb, self.opts, self.log = oeb, opts, log - - from calibre.ebooks.oeb.base import XPath, OEB_STYLES - - stylesheet = None - for item in self.oeb.manifest: - if item.media_type.lower() in OEB_STYLES: - stylesheet = item.data - break - - if stylesheet is None: - return - - - top_level_elements = {} - second_level_elements = {} - - for x in self.oeb.spine: - root = x.data - body = XPath('//h:body')(root) - if body: - body = body[0] - - if not hasattr(body, 'xpath'): - continue - - # Check for margins on top level elements - for lb in XPath('./h:div|./h:p|./*/h:div|./*/h:p')(body): - cls = lb.get('class', '') - level = top_level_elements if lb.getparent() is body else \ - second_level_elements - if cls not in level: - level[cls] = [] - top_level_elements[cls] = [] - level[cls].append(lb) - - - def get_margins(self, stylesheet, cls): - pass - diff --git a/src/calibre/ebooks/oeb/transforms/page_margin.py b/src/calibre/ebooks/oeb/transforms/page_margin.py index 589f004dd1..c415dda0e0 100644 --- a/src/calibre/ebooks/oeb/transforms/page_margin.py +++ b/src/calibre/ebooks/oeb/transforms/page_margin.py @@ -11,6 +11,26 @@ from collections import Counter from calibre.ebooks.oeb.base import OEB_STYLES, barename, XPath +class RemoveAdobeMargins(object): + ''' + Remove margins specified in Adobe's page templates. + ''' + + def __call__(self, oeb, log, opts): + self.oeb, self.opts, self.log = oeb, opts, log + + for item in self.oeb.manifest: + if item.media_type == 'application/vnd.adobe-page-template+xml': + self.log('Removing page margins specified in the' + ' Adobe page template') + for elem in item.data.xpath( + '//*[@margin-bottom or @margin-top ' + 'or @margin-left or @margin-right]'): + for margin in ('left', 'right', 'top', 'bottom'): + attr = 'margin-'+margin + elem.attrib.pop(attr, None) + + class RemoveFakeMargins(object): ''' From eb38a529df6241158371f84d68dd49469de61dac Mon Sep 17 00:00:00 2001 From: GRiker Date: Mon, 28 Mar 2011 14:58:10 -0700 Subject: [PATCH 34/37] GwR patch supporting new user Apple driver disabling --- src/calibre/devices/apple/driver.py | 81 +++++++++++++++++++++-------- src/calibre/devices/errors.py | 4 +- src/calibre/gui2/device.py | 2 +- 3 files changed, 63 insertions(+), 24 deletions(-) diff --git a/src/calibre/devices/apple/driver.py b/src/calibre/devices/apple/driver.py index c12b533c4e..ed26ae86e1 100644 --- a/src/calibre/devices/apple/driver.py +++ b/src/calibre/devices/apple/driver.py @@ -17,35 +17,75 @@ from calibre.ebooks.metadata import authors_to_string, MetaInformation, \ title_sort from calibre.ebooks.metadata.book.base import Metadata from calibre.ebooks.metadata.epub import set_metadata +from calibre.gui2.dialogs.confirm_delete import _config_name from calibre.library.server.utils import strftime -from calibre.utils.config import config_dir, prefs +from calibre.utils.config import config_dir, dynamic, DynamicConfig, prefs from calibre.utils.date import now, parse_date from calibre.utils.logging import Log from calibre.utils.zipfile import ZipFile - class AppleOpenFeedback(OpenFeedback): - def __init__(self): + def __init__(self, plugin): OpenFeedback.__init__(self, u'') + self.log = plugin.log + self.plugin = plugin def custom_dialog(self, parent): - from PyQt4.Qt import (QDialog, QVBoxLayout, QLabel, QDialogButtonBox) + from PyQt4.Qt import (QCheckBox, QDialog, QDialogButtonBox, QIcon, + QLabel, QPixmap, QPushButton, QSize, QVBoxLayout) class Dialog(QDialog): - def __init__(self, p): + def __init__(self, p, pixmap='dialog_information.png'): QDialog.__init__(self, p) + + self.setWindowTitle("Apple iDevice detected") self.l = l = QVBoxLayout() self.setLayout(l) - l.addWidget(QLabel('test')) - self.bb = QDialogButtonBox(QDialogButtonBox.OK) + msg = QLabel() + msg.setText( + '

If you do not want calibre to recognize your Apple iDevice ' + 'when it is connected to your computer, ' + 'click Disable Apple Driver.

' + '

To transfer books to your iDevice, ' + 'click Disable Apple Driver, ' + "then use the 'Connect to iTunes' method recommended in the " + 'Calibre + iDevices FAQ, ' + 'using the Connect/Share|Connect to iTunes menu item.

' + '

Enabling the Apple driver for direct connection to iDevices ' + 'is an unsupported advanced user mode.

' + '

' + ) + msg.setWordWrap(True) + l.addWidget(msg) + + self.bb = QDialogButtonBox() + disable_driver = QPushButton(_("Disable Apple driver")) + disable_driver.setDefault(True) + self.bb.addButton(disable_driver, QDialogButtonBox.RejectRole) + + enable_driver = QPushButton(_("Enable Apple driver")) + self.bb.addButton(enable_driver, QDialogButtonBox.AcceptRole) l.addWidget(self.bb) self.bb.accepted.connect(self.accept) self.bb.rejected.connect(self.reject) - return Dialog(parent) + self.setWindowIcon(QIcon(I(pixmap))) + self.resize(self.sizeHint()) + + if Dialog(parent).exec_(): + # Enable Apple driver, inhibit future display of dialog + # Reset dialog with Preferences|Behavior|Reset all disabled confirmation dialogs + self.log.info(" Apple driver ENABLED") + dynamic[_config_name(self.plugin.DISPLAY_DISABLE_DIALOG)] = False + else: + # Disable Apple driver + from calibre.customize.ui import disable_plugin + self.log.info(" Apple driver DISABLED") + disable_plugin(self.plugin) + from PIL import Image as PILImage from lxml import etree @@ -77,15 +117,11 @@ class DriverBase(DeviceConfig, DevicePlugin): 'iBooks Category'), _('Cache covers from iTunes/iBooks') + ':::' + - _('Enable to cache and display covers from iTunes/iBooks'), - _("Skip 'Connect to iTunes' recommendation") + - ':::' + - _("Enable to skip the 'Connect to iTunes' recommendation dialog") + _('Enable to cache and display covers from iTunes/iBooks') ] EXTRA_CUSTOMIZATION_DEFAULT = [ True, True, - False, ] @@ -141,12 +177,13 @@ class ITUNES(DriverBase): supported_platforms = ['osx','windows'] author = 'GRiker' #: The version of this plugin as a 3-tuple (major, minor, revision) - version = (0,9,0) + version = (1,0,0) + + DISPLAY_DISABLE_DIALOG = "display_disable_dialog" # EXTRA_CUSTOMIZATION_MESSAGE indexes USE_SERIES_AS_CATEGORY = 0 CACHE_COVERS = 1 - SKIP_CONNECT_TO_ITUNES_DIALOG = 2 OPEN_FEEDBACK_MESSAGE = _( 'Apple device detected, launching iTunes, please wait ...') @@ -762,15 +799,17 @@ class ITUNES(DriverBase): Note that most of the initialization is necessarily performed in can_handle(), as we need to talk to iTunes to discover if there's a connected iPod ''' + if DEBUG: self.log.info("ITUNES.open()") - # Display a dialog recommending using 'Connect to iTunes' - if not self.settings().extra_customization[self.SKIP_CONNECT_TO_ITUNES_DIALOG]: - raise AppleOpenFeedback() - - if DEBUG: - self.log.info(" advanced user mode, directly connecting to iDevice") + # Display a dialog recommending using 'Connect to iTunes' if user hasn't + # previously disabled the dialog + if dynamic.get(_config_name(self.DISPLAY_DISABLE_DIALOG),True): + raise AppleOpenFeedback(self) + else: + if DEBUG: + self.log.info(" advanced user mode, directly connecting to iDevice") # Confirm/create thumbs archive if not os.path.exists(self.cache_dir): diff --git a/src/calibre/devices/errors.py b/src/calibre/devices/errors.py index 7b11b6933f..05c30c2f72 100644 --- a/src/calibre/devices/errors.py +++ b/src/calibre/devices/errors.py @@ -43,8 +43,8 @@ class OpenFeedback(DeviceError): def custom_dialog(self, parent): ''' - If you need to show the user a custom dialog, instead if just - displaying the feedback_msg, create and return it here. + If you need to show the user a custom dialog, create and + run it from a custom_dialog() method in your subclass. ''' raise NotImplementedError diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 6163c01d27..39a638850d 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -623,7 +623,7 @@ class DeviceMixin(object): # {{{ self.__of_dev_mem__ = d = e.custom_dialog(self) except NotImplementedError: self.__of_dev_mem__ = d = info_dialog(self, devname, e.feedback_msg) - d.show() + d.show() def auto_convert_question(self, msg, autos): autos = u'\n'.join(map(unicode, map(force_unicode, autos))) From 833f46b1496ba6daaee527cbcc3f4eae6d919ea6 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 28 Mar 2011 16:26:54 -0600 Subject: [PATCH 35/37] ... --- src/calibre/ebooks/metadata/sources/identify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/ebooks/metadata/sources/identify.py b/src/calibre/ebooks/metadata/sources/identify.py index 53fb3a9ea4..5bc0c5b256 100644 --- a/src/calibre/ebooks/metadata/sources/identify.py +++ b/src/calibre/ebooks/metadata/sources/identify.py @@ -79,7 +79,7 @@ def identify(log, abort, title=None, authors=None, identifiers=[], timeout=30): time.sleep(0.2) if get_results() and first_result_at is None: - first_result_at = time.time() + first_result_at = time.time() if not is_worker_alive(workers): break From 4952ef92ec5fe7fb781cdaba5c039ebd6d6d9ad1 Mon Sep 17 00:00:00 2001 From: GRiker Date: Mon, 28 Mar 2011 16:03:30 -0700 Subject: [PATCH 36/37] GwR patches supporting dynamic disabling of Apple driver V2 --- src/calibre/devices/apple/driver.py | 14 +++++++------- src/calibre/devices/errors.py | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/calibre/devices/apple/driver.py b/src/calibre/devices/apple/driver.py index 7a9562f136..2dc095507c 100644 --- a/src/calibre/devices/apple/driver.py +++ b/src/calibre/devices/apple/driver.py @@ -38,9 +38,9 @@ class AppleOpenFeedback(OpenFeedback): class Dialog(QDialog): - def __init__(self, p, pixmap='dialog_information.png'): + def __init__(self, p, cd, pixmap='dialog_information.png'): QDialog.__init__(self, p) - + self.cd = cd self.setWindowTitle("Apple iDevice detected") self.l = l = QVBoxLayout() self.setLayout(l) @@ -80,14 +80,14 @@ class AppleOpenFeedback(OpenFeedback): def do_it(self, return_code): if return_code == self.Accepted: - self.log.info(" Apple driver ENABLED") - dynamic[config_name(self.plugin.DISPLAY_DISABLE_DIALOG)] = False + self.cd.log.info(" Apple driver ENABLED") + dynamic[config_name(self.cd.plugin.DISPLAY_DISABLE_DIALOG)] = False else: from calibre.customize.ui import disable_plugin - self.log.info(" Apple driver DISABLED") - disable_plugin(self.plugin) + self.cd.log.info(" Apple driver DISABLED") + disable_plugin(self.cd.plugin) - return Dialog(parent) + return Dialog(parent, self) from PIL import Image as PILImage diff --git a/src/calibre/devices/errors.py b/src/calibre/devices/errors.py index 7b11b6933f..ecd61a1169 100644 --- a/src/calibre/devices/errors.py +++ b/src/calibre/devices/errors.py @@ -43,7 +43,7 @@ class OpenFeedback(DeviceError): def custom_dialog(self, parent): ''' - If you need to show the user a custom dialog, instead if just + If you need to show the user a custom dialog, instead of just displaying the feedback_msg, create and return it here. ''' raise NotImplementedError From 1e82b4fcefa9a2257a2bcb98a3c840237966bb2b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 28 Mar 2011 17:04:05 -0600 Subject: [PATCH 37/37] ... --- src/calibre/devices/apple/driver.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/calibre/devices/apple/driver.py b/src/calibre/devices/apple/driver.py index 7a9562f136..7ede6dbf36 100644 --- a/src/calibre/devices/apple/driver.py +++ b/src/calibre/devices/apple/driver.py @@ -17,7 +17,6 @@ from calibre.ebooks.metadata import authors_to_string, MetaInformation, \ title_sort from calibre.ebooks.metadata.book.base import Metadata from calibre.ebooks.metadata.epub import set_metadata -from calibre.gui2.dialogs.confirm_delete import config_name from calibre.library.server.utils import strftime from calibre.utils.config import config_dir, dynamic, prefs from calibre.utils.date import now, parse_date @@ -80,6 +79,7 @@ class AppleOpenFeedback(OpenFeedback): def do_it(self, return_code): if return_code == self.Accepted: + from calibre.gui2.dialogs.confirm_delete import config_name self.log.info(" Apple driver ENABLED") dynamic[config_name(self.plugin.DISPLAY_DISABLE_DIALOG)] = False else: @@ -808,6 +808,7 @@ class ITUNES(DriverBase): # Display a dialog recommending using 'Connect to iTunes' if user hasn't # previously disabled the dialog + from calibre.gui2.dialogs.confirm_delete import config_name if dynamic.get(config_name(self.DISPLAY_DISABLE_DIALOG),True): raise AppleOpenFeedback(self) else: