From 07a63b43625761961c6e20a301bbda012920d23a Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 13 Aug 2010 23:06:52 -0600 Subject: [PATCH 01/73] Remove ununsed cxFreeze --- setup/installer/cx_Freeze/HISTORY.txt | 244 -------- setup/installer/cx_Freeze/LICENSE.txt | 53 -- setup/installer/cx_Freeze/MANIFEST.in | 6 - setup/installer/cx_Freeze/PKG-INFO | 22 - setup/installer/cx_Freeze/README.txt | 12 - .../installer/cx_Freeze/cx_Freeze/__init__.py | 14 - setup/installer/cx_Freeze/cx_Freeze/dist.py | 279 --------- setup/installer/cx_Freeze/cx_Freeze/finder.py | 455 --------------- .../installer/cx_Freeze/cx_Freeze/freezer.py | 550 ------------------ setup/installer/cx_Freeze/cx_Freeze/hooks.py | 281 --------- setup/installer/cx_Freeze/cx_Freeze/main.py | 171 ------ .../installer/cx_Freeze/cx_Freeze/windist.py | 337 ----------- setup/installer/cx_Freeze/cxfreeze | 6 - .../cx_Freeze/initscripts/Console.py | 35 -- .../cx_Freeze/initscripts/ConsoleKeepPath.py | 19 - .../initscripts/ConsoleSetLibPath.py | 38 -- .../cx_Freeze/initscripts/SharedLib.py | 20 - .../cx_Freeze/initscripts/SharedLibSource.py | 23 - .../cx_Freeze/samples/advanced/advanced_1.py | 7 - .../cx_Freeze/samples/advanced/advanced_2.py | 7 - .../samples/advanced/modules/testfreeze_1.py | 1 - .../samples/advanced/modules/testfreeze_2.py | 1 - .../cx_Freeze/samples/advanced/setup.py | 31 - .../cx_Freeze/samples/matplotlib/setup.py | 27 - .../samples/matplotlib/test_matplotlib.py | 48 -- .../samples/relimport/pkg1/__init__.py | 3 - .../samples/relimport/pkg1/pkg2/__init__.py | 3 - .../samples/relimport/pkg1/pkg2/sub3.py | 3 - .../samples/relimport/pkg1/pkg2/sub5.py | 1 - .../cx_Freeze/samples/relimport/pkg1/sub1.py | 2 - .../cx_Freeze/samples/relimport/pkg1/sub2.py | 1 - .../cx_Freeze/samples/relimport/pkg1/sub4.py | 1 - .../cx_Freeze/samples/relimport/pkg1/sub6.py | 1 - .../cx_Freeze/samples/relimport/relimport.py | 1 - .../cx_Freeze/samples/relimport/setup.py | 16 - .../cx_Freeze/samples/simple/hello.py | 19 - .../cx_Freeze/samples/simple/setup.py | 18 - setup/installer/cx_Freeze/samples/wx/setup.py | 25 - setup/installer/cx_Freeze/samples/wx/wxapp.py | 42 -- setup/installer/cx_Freeze/setup.py | 197 ------- .../installer/cx_Freeze/source/bases/Common.c | 262 --------- .../cx_Freeze/source/bases/Console.c | 72 --- .../cx_Freeze/source/bases/ConsoleKeepPath.c | 60 -- .../cx_Freeze/source/bases/Win32GUI.c | 242 -------- .../installer/cx_Freeze/source/bases/dummy.rc | 5 - .../cx_Freeze/source/bases/manifest.rc | 3 - setup/installer/cx_Freeze/source/util.c | 418 ------------- 47 files changed, 4082 deletions(-) delete mode 100644 setup/installer/cx_Freeze/HISTORY.txt delete mode 100644 setup/installer/cx_Freeze/LICENSE.txt delete mode 100644 setup/installer/cx_Freeze/MANIFEST.in delete mode 100644 setup/installer/cx_Freeze/PKG-INFO delete mode 100644 setup/installer/cx_Freeze/README.txt delete mode 100644 setup/installer/cx_Freeze/cx_Freeze/__init__.py delete mode 100644 setup/installer/cx_Freeze/cx_Freeze/dist.py delete mode 100644 setup/installer/cx_Freeze/cx_Freeze/finder.py delete mode 100644 setup/installer/cx_Freeze/cx_Freeze/freezer.py delete mode 100644 setup/installer/cx_Freeze/cx_Freeze/hooks.py delete mode 100644 setup/installer/cx_Freeze/cx_Freeze/main.py delete mode 100644 setup/installer/cx_Freeze/cx_Freeze/windist.py delete mode 100755 setup/installer/cx_Freeze/cxfreeze delete mode 100755 setup/installer/cx_Freeze/initscripts/Console.py delete mode 100755 setup/installer/cx_Freeze/initscripts/ConsoleKeepPath.py delete mode 100755 setup/installer/cx_Freeze/initscripts/ConsoleSetLibPath.py delete mode 100755 setup/installer/cx_Freeze/initscripts/SharedLib.py delete mode 100755 setup/installer/cx_Freeze/initscripts/SharedLibSource.py delete mode 100644 setup/installer/cx_Freeze/samples/advanced/advanced_1.py delete mode 100644 setup/installer/cx_Freeze/samples/advanced/advanced_2.py delete mode 100644 setup/installer/cx_Freeze/samples/advanced/modules/testfreeze_1.py delete mode 100644 setup/installer/cx_Freeze/samples/advanced/modules/testfreeze_2.py delete mode 100644 setup/installer/cx_Freeze/samples/advanced/setup.py delete mode 100644 setup/installer/cx_Freeze/samples/matplotlib/setup.py delete mode 100644 setup/installer/cx_Freeze/samples/matplotlib/test_matplotlib.py delete mode 100644 setup/installer/cx_Freeze/samples/relimport/pkg1/__init__.py delete mode 100644 setup/installer/cx_Freeze/samples/relimport/pkg1/pkg2/__init__.py delete mode 100644 setup/installer/cx_Freeze/samples/relimport/pkg1/pkg2/sub3.py delete mode 100644 setup/installer/cx_Freeze/samples/relimport/pkg1/pkg2/sub5.py delete mode 100644 setup/installer/cx_Freeze/samples/relimport/pkg1/sub1.py delete mode 100644 setup/installer/cx_Freeze/samples/relimport/pkg1/sub2.py delete mode 100644 setup/installer/cx_Freeze/samples/relimport/pkg1/sub4.py delete mode 100644 setup/installer/cx_Freeze/samples/relimport/pkg1/sub6.py delete mode 100644 setup/installer/cx_Freeze/samples/relimport/relimport.py delete mode 100644 setup/installer/cx_Freeze/samples/relimport/setup.py delete mode 100644 setup/installer/cx_Freeze/samples/simple/hello.py delete mode 100644 setup/installer/cx_Freeze/samples/simple/setup.py delete mode 100644 setup/installer/cx_Freeze/samples/wx/setup.py delete mode 100644 setup/installer/cx_Freeze/samples/wx/wxapp.py delete mode 100755 setup/installer/cx_Freeze/setup.py delete mode 100644 setup/installer/cx_Freeze/source/bases/Common.c delete mode 100644 setup/installer/cx_Freeze/source/bases/Console.c delete mode 100644 setup/installer/cx_Freeze/source/bases/ConsoleKeepPath.c delete mode 100644 setup/installer/cx_Freeze/source/bases/Win32GUI.c delete mode 100644 setup/installer/cx_Freeze/source/bases/dummy.rc delete mode 100644 setup/installer/cx_Freeze/source/bases/manifest.rc delete mode 100644 setup/installer/cx_Freeze/source/util.c diff --git a/setup/installer/cx_Freeze/HISTORY.txt b/setup/installer/cx_Freeze/HISTORY.txt deleted file mode 100644 index acf9ad0dfe..0000000000 --- a/setup/installer/cx_Freeze/HISTORY.txt +++ /dev/null @@ -1,244 +0,0 @@ -Changes from 4.0 to 4.0.1 - 1) Added support for Python 2.6. On Windows a manifest file is now required - because of the switch to using the new Microsoft C runtime. - 2) Ensure that hooks are run for builtin modules. - -Changes from 4.0b1 to 4.0 - 1) Added support for copying files to the target directory. - 2) Added support for a hook that runs when a module is missing. - 3) Added support for binary path includes as well as excludes; use sequences - rather than dictionaries as a more convenient API; exclude the standard - locations for 32-bit and 64-bit libaries in multi-architecture systems. - 4) Added support for searching zip files (egg files) for modules. - 5) Added support for handling system exit exceptions similarly to what Python - does itself as requested by Sylvain. - 6) Added code to wait for threads to shut down like the normal Python - interpreter does. Thanks to Mariano Disanzo for discovering this - discrepancy. - 7) Hooks added or modified based on feedback from many people. - 8) Don't include the version name in the display name of the MSI. - 9) Use the OS dependent path normalization routines rather than simply use the - lowercase value as on Unix case is important; thanks to Artie Eoff for - pointing this out. -10) Include a version attribute in the cx_Freeze package and display it in the - output for the --version option to the script. -11) Include build instructions as requested by Norbert Sebok. -12) Add support for copying files when modules are included which require data - files to operate properly; add support for copying the necessary files for - the Tkinter and matplotlib modules. -13) Handle deferred imports recursively as needed; ensure that from lists do - not automatically indicate that they are part of the module or the deferred - import processing doesn't actually work! -14) Handle the situation where a module imports everything from a package and - the __all__ variable has been defined but the package has not actually - imported everything in the __all__ variable during initialization. -15) Modified license text to more closely match the Python Software Foundation - license as was intended. -16) Added sample script for freezing an application using matplotlib. -17) Renamed freeze to cxfreeze to avoid conflict with another package that uses - that executable as requested by Siegfried Gevatter. - -Changes from 3.0.3 to 4.0b1 - 1) Added support for placing modules in library.zip or in a separate zip file - for each executable that is produced. - 2) Added support for copying binary dependent files (DLLs and shared - libraries) - 3) Added support for including all submodules in a package - 4) Added support for including icons in Windows executables - 5) Added support for constants module which can be used for determining - certain build constants at runtime - 6) Added support for relative imports available in Python 2.5 and up - 7) Added support for building Windows installers (Python 2.5 and up) and - RPM packages - 8) Added support for distutils configuration scripts - 9) Added support for hooks which can force inclusion or exclusion of modules - when certain modules are included -10) Added documentation and samples -11) Added setup.py for building the cx_Freeze package instead of a script - used to build only the frozen bases -12) FreezePython renamed to a script called freeze in the Python distribution -13) On Linux and other platforms that support it set LD_RUN_PATH to include - the directory in which the executable is located - -Changes from 3.0.2 to 3.0.3 - 1) In Common.c, used MAXPATHLEN defined in the Python OS independent include - file rather than the PATH_MAX define which is OS dependent and is not - available on IRIX as noted by Andrew Jones. - 2) In the initscript ConsoleSetLibPath.py, added lines from initscript - Console.py that should have been there since the only difference between - that script and this one is the automatic re-execution of the executable. - 3) Added an explicit "import encodings" to the initscripts in order to handle - Unicode encodings a little better. Thanks to Ralf Schmitt for pointing out - the problem and its solution. - 4) Generated a meaningful name for the extension loader script so that it is - clear which particular extension module is being loaded when an exception - is being raised. - 5) In MakeFrozenBases.py, use distutils to figure out a few more - platform-dependent linker flags as suggested by Ralf Schmitt. - -Changes from 3.0.1 to 3.0.2 - 1) Add support for compressing the byte code in the zip files that are - produced. - 2) Add better support for the win32com package as requested by Barry Scott. - 3) Prevent deletion of target file if it happens to be identical to the - source file. - 4) Include additional flags for local modifications to a Python build as - suggested by Benjamin Rutt. - 5) Expanded instructions for building cx_Freeze from source based on a - suggestion from Gregg Lind. - 6) Fix typo in help string. - -Changes from 3.0 to 3.0.1 - 1) Added option --default-path which is used to specify the path used when - finding modules. This is particularly useful when performing cross - compilations (such as for building a frozen executable for Windows CE). - 2) Added option --shared-lib-name which can be used to specify the name of - the shared library (DLL) implementing the Python runtime that is required - for the frozen executable to work. This option is also particularly useful - when cross compiling since the normal method for determining this - information cannot be used. - 3) Added option --zip-include which allows for additional files to be added - to the zip file that contains the modules that implement the Python - script. Thanks to Barray Warsaw for providing the initial patch. - 4) Added support for handling read-only files properly. Thanks to Peter - Grayson for pointing out the problem and providing a solution. - 5) Added support for a frozen executable to be a symbolic link. Thanks to - Robert Kiendl for providing the initial patch. - 6) Enhanced the support for running a frozen executable that uses an existing - Python installation to locate modules it requires. This is primarily of - use for embedding Python where the interface is C but the ability to run - from source is still desired. - 7) Modified the documentation to indicate that building from source on - Windows currently requires the mingw compiler (http://www.mingw.org). - 8) Workaround the problem in Python 2.3 (fixed in Python 2.4) which causes a - broken module to be left in sys.modules if an ImportError takes place - during the execution of the code in that module. Thanks to Roger Binns - for pointing this out. - -Changes from 3.0 beta3 to 3.0 - 1) Ensure that ldd is only run on extension modules. - 2) Allow for using a compiler other than gcc for building the frozen base - executables by setting the environment variable CC. - 3) Ensure that the import lock is not held while executing the main script; - otherwise, attempts to import a module within a thread will hang that - thread as noted by Roger Binns. - 4) Added support for replacing the paths in all frozen modules with something - else (so that for example the path of the machine on which the freezing - was done is not displayed in tracebacks) - -Changes from 3.0 beta2 to 3.0 beta3 - 1) Explicitly include the warnings module so that at runtime warnings are - suppressed as when running Python normally. - 2) Improve the extension loader so that an ImportError is raised when the - dynamic module is not located; otherwise an error about missing attributes - is raised instead. - 3) Extension loaders are only created when copying dependencies since the - normal module should be loadable in the situation where a Python - installation is available. - 4) Added support for Python 2.4. - 5) Fixed the dependency checking for wxPython to be a little more - intelligent. - -Changes from 3.0 beta1 to 3.0 beta2 - 1) Fix issues with locating the initscripts and bases relative to the - directory in which the executable was started. - 2) Added new base executable ConsoleKeepPath which is used when an existing - Python installation is required (such as for FreezePython itself). - 3) Forced the existence of a Python installation to be ignored when using the - standard Console base executable. - 4) Remove the existing file when copying dependent files; otherwise, an error - is raised when attempting to overwrite read-only files. - 5) Added option -O (or -OO) to FreezePython to set the optimization used when - generating bytecode. - -Changes from 2.2 to 3.0 beta1 - 1) cx_Freeze now requires Python 2.3 or higher since it takes advantage of - the ability of Python 2.3 and higher to import modules from zip files. - This makes the freezing process considerably simpler and also allows for - the execution of multiple frozen packages (such as found in COM servers or - shared libraries) without requiring modification to the Python modules. - 2) All external dependencies have been removed. cx_Freeze now only requires - a standard Python distribution to do its work. - 3) Added the ability to define the initialization scripts that cx_Freeze uses - on startup of the frozen program. Previously, these scripts were written - in C and could not easily be changed; now they are written in Python and - can be found in the initscripts directory (and chosen with the - new --init-script option to FreezePython). - 4) The base executable ConsoleSetLibPath has been removed and replaced with - the initscript ConsoleSetLibPath. - 5) Removed base executables for Win32 services and Win32 COM servers. This - functionality will be restored in the future but it is not currently in a - state that is ready for release. If this functionality is required, please - use py2exe or contact me for my work in progress. - 6) The attribute sys.frozen is now set so that more recent pywin32 modules - work as expected when frozen. - 7) Added option --include-path to FreezePython to allow overriding of - sys.path without modifying the environment variable PYTHONPATH. - 8) Added option --target-dir/--install-dir to specify the directory in which - the frozen executable and its dependencies will be placed. - 9) Removed the option --shared-lib since it was used for building shared - libraries and can be managed with the initscript SharedLib.py. -10) MakeFrozenBases.py now checks the platform specific include directory as - requested by Michael Partridge. - - -Changes from 2.1 to 2.2 - 1) Add option (--ext-list-file) to FreezePython to write the list of - extensions copied to the installation directory to a file. This option is - useful in cases where multiple builds are performed into the same - installation directory. - 2) Pass the arguments on the command line through to Win32 GUI applications. - Thanks to Michael Porter for pointing this out. - 3) Link directly against the python DLL when building the frozen bases on - Windows, thus eliminating the need for building an import library. - 4) Force sys.path to include the directory in which the script to be frozen - is found. - 5) Make sure that the installation directory exists before attempting to - copy the target binary into it. - 6) The Win32GUI base has been modified to display fatal errors in message - boxes, rather than printing errors to stderr, since on Windows the - standard file IO handles are all closed. - -Changes from 2.0 to 2.1 - 1) Remove dependency on Python 2.2. Thanks to Paul Moore for not only - pointing it out but providing patches. - 2) Set up the list of frozen modules in advance, rather than doing it after - Python is initialized so that implicit imports done by Python can be - satisfied. The bug in Python 2.3 that demonstrated this issue has been - fixed in the first release candidate. Thanks to Thomas Heller for pointing - out the obvious in this instance! - 3) Added additional base executable (ConsoleSetLibPath) to support setting - the LD_LIBRARY_PATH variable on Unix platforms and restarting the - executable to put the new setting into effect. This is primarily of use - in distributing wxPython applications on Unix where the shared library - has an embedded RPATH value which can cause problems. - 4) Small improvements of documentation based on feedback from several people. - 5) Print information about the files written or copied during the freezing - process. - 6) Do not copy extensions when freezing if the path is being overridden since - it is expected that a full Python installation is available to the target - users of the frozen binary. - 7) Provide meaningful error message when the wxPython library cannot be - found during the freezing process. - -Changes from 1.1 to 2.0 - 1) Added support for in process (DLL) COM servers using PythonCOM. - 2) Ensured that the frozen flag is set prior to determining the full path for - the program in order to avoid warnings about Python not being found on - some platforms. - 3) Added include file and resource file to the source tree to avoid the - dependency on the Wine message compiler for Win32 builds. - 4) Dropped the option --copy-extensions; this now happens automatically since - the resulting binary is useless without them. - 5) Added a sample for building a Win32 service. - 6) Make use of improved modules from Python 2.3 (which function under 2.2) - -Changes from 1.0 to 1.1 - 1) Fixed import error with C extensions in packages; thanks to Thomas Heller - for pointing out the solution to this problem. - 2) Added options to FreezePython to allow for the inclusion of modules which - will not be found by the module finder (--include-modules) and the - exclusion of modules which will be found by the module finder but should - not be included (--exclude-modules). - 3) Fixed typo in README.txt. - diff --git a/setup/installer/cx_Freeze/LICENSE.txt b/setup/installer/cx_Freeze/LICENSE.txt deleted file mode 100644 index cb9ee05a8a..0000000000 --- a/setup/installer/cx_Freeze/LICENSE.txt +++ /dev/null @@ -1,53 +0,0 @@ -Copyright © 2007-2008, Colt Engineering, Edmonton, Alberta, Canada. -Copyright © 2001-2006, Computronix (Canada) Ltd., Edmonton, Alberta, Canada. -All rights reserved. - -NOTE: this license is derived from the Python Software Foundation License -which can be found at http://www.python.org/psf/license - -License for cx_Freeze 4.0.1 ---------------------------- - -1. This LICENSE AGREEMENT is between the copyright holders and the Individual - or Organization ("Licensee") accessing and otherwise using cx_Freeze - software in source or binary form and its associated documentation. - -2. Subject to the terms and conditions of this License Agreement, the - copyright holders hereby grant Licensee a nonexclusive, royalty-free, - world-wide license to reproduce, analyze, test, perform and/or display - publicly, prepare derivative works, distribute, and otherwise use cx_Freeze - alone or in any derivative version, provided, however, that this License - Agreement and this notice of copyright are retained in cx_Freeze alone or in - any derivative version prepared by Licensee. - -3. In the event Licensee prepares a derivative work that is based on or - incorporates cx_Freeze or any part thereof, and wants to make the derivative - work available to others as provided herein, then Licensee hereby agrees to - include in any such work a brief summary of the changes made to cx_Freeze. - -4. The copyright holders are making cx_Freeze available to Licensee on an - "AS IS" basis. THE COPYRIGHT HOLDERS MAKE NO REPRESENTATIONS OR WARRANTIES, - EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, THE COPYRIGHT - HOLDERS MAKE NO AND DISCLAIM ANY REPRESENTATION OR WARRANTY OF - MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF - CX_FREEZE WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. - -5. THE COPYRIGHT HOLDERS SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF - CX_FREEZE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS - A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING CX_FREEZE, OR ANY - DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. - -6. This License Agreement will automatically terminate upon a material breach - of its terms and conditions. - -7. Nothing in this License Agreement shall be deemed to create any relationship - of agency, partnership, or joint venture between the copyright holders and - Licensee. This License Agreement does not grant permission to use - copyright holder's trademarks or trade name in a trademark sense to endorse - or promote products or services of Licensee, or any third party. - -8. By copying, installing or otherwise using cx_Freeze, Licensee agrees to be - bound by the terms and conditions of this License Agreement. - -Computronix® is a registered trademark of Computronix (Canada) Ltd. - diff --git a/setup/installer/cx_Freeze/MANIFEST.in b/setup/installer/cx_Freeze/MANIFEST.in deleted file mode 100644 index 2348a66973..0000000000 --- a/setup/installer/cx_Freeze/MANIFEST.in +++ /dev/null @@ -1,6 +0,0 @@ -include MANIFEST.in -include *.txt -recursive-include doc *.html -recursive-include initscripts *.py -recursive-include samples *.py -recursive-include source *.c *.rc diff --git a/setup/installer/cx_Freeze/PKG-INFO b/setup/installer/cx_Freeze/PKG-INFO deleted file mode 100644 index aa53b57914..0000000000 --- a/setup/installer/cx_Freeze/PKG-INFO +++ /dev/null @@ -1,22 +0,0 @@ -Metadata-Version: 1.0 -Name: cx_Freeze -Version: 4.0.1 -Summary: create standalone executables from Python scripts -Home-page: http://cx-freeze.sourceforge.net -Author: Anthony Tuininga -Author-email: anthony.tuininga@gmail.com -License: Python Software Foundation License -Description: create standalone executables from Python scripts -Keywords: freeze -Platform: UNKNOWN -Classifier: Development Status :: 5 - Production/Stable -Classifier: Intended Audience :: Developers -Classifier: License :: OSI Approved :: Python Software Foundation License -Classifier: Natural Language :: English -Classifier: Operating System :: OS Independent -Classifier: Programming Language :: C -Classifier: Programming Language :: Python -Classifier: Topic :: Software Development :: Build Tools -Classifier: Topic :: Software Development :: Libraries :: Python Modules -Classifier: Topic :: System :: Software Distribution -Classifier: Topic :: Utilities diff --git a/setup/installer/cx_Freeze/README.txt b/setup/installer/cx_Freeze/README.txt deleted file mode 100644 index 1ac67dc749..0000000000 --- a/setup/installer/cx_Freeze/README.txt +++ /dev/null @@ -1,12 +0,0 @@ -Please see cx_Freeze.html for documentation on how to use cx_Freeze. - -To build: - -python setup.py build -python setup.py install - -On Windows I have used the MinGW compiler (http://www.mingw.org) - -python setup.py build --compiler=mingw32 -python setup.py build --compiler=mingw32 install - diff --git a/setup/installer/cx_Freeze/cx_Freeze/__init__.py b/setup/installer/cx_Freeze/cx_Freeze/__init__.py deleted file mode 100644 index 545883eb3e..0000000000 --- a/setup/installer/cx_Freeze/cx_Freeze/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -version = "4.0.1" - -import sys -from dist import * -if sys.platform == "win32" and sys.version_info[:2] >= (2, 5): - from windist import * -from finder import * -from freezer import * -from main import * - -del dist -del finder -del freezer - diff --git a/setup/installer/cx_Freeze/cx_Freeze/dist.py b/setup/installer/cx_Freeze/cx_Freeze/dist.py deleted file mode 100644 index c2af2ac623..0000000000 --- a/setup/installer/cx_Freeze/cx_Freeze/dist.py +++ /dev/null @@ -1,279 +0,0 @@ -import distutils.command.bdist_rpm -import distutils.command.build -import distutils.command.install -import distutils.core -import distutils.dir_util -import distutils.dist -import distutils.util -import distutils.version -import os -import sys - -import cx_Freeze - -__all__ = [ "bdist_rpm", "build", "build_exe", "install", "install_exe", - "setup" ] - -class Distribution(distutils.dist.Distribution): - - def __init__(self, attrs): - self.executables = [] - distutils.dist.Distribution.__init__(self, attrs) - - -class bdist_rpm(distutils.command.bdist_rpm.bdist_rpm): - - def finalize_options(self): - distutils.command.bdist_rpm.bdist_rpm.finalize_options(self) - self.use_rpm_opt_flags = 1 - - def _make_spec_file(self): - contents = distutils.command.bdist_rpm.bdist_rpm._make_spec_file(self) - return [c for c in contents if c != 'BuildArch: noarch'] - - -class build(distutils.command.build.build): - user_options = distutils.command.build.build.user_options + [ - ('build-exe=', None, 'build directory for executables') - ] - - def get_sub_commands(self): - subCommands = distutils.command.build.build.get_sub_commands(self) - if self.distribution.executables: - subCommands.append("build_exe") - return subCommands - - def initialize_options(self): - distutils.command.build.build.initialize_options(self) - self.build_exe = None - - def finalize_options(self): - distutils.command.build.build.finalize_options(self) - if self.build_exe is None: - dirName = "exe.%s-%s" % \ - (distutils.util.get_platform(), sys.version[0:3]) - self.build_exe = os.path.join(self.build_base, dirName) - - -class build_exe(distutils.core.Command): - description = "build executables from Python scripts" - user_options = [ - ('build-exe=', 'b', - 'directory for built executables'), - ('optimize=', 'O', - 'optimization level: -O1 for "python -O", ' - '-O2 for "python -OO" and -O0 to disable [default: -O0]'), - ('excludes=', 'e', - 'comma-separated list of modules to exclude'), - ('includes=', 'i', - 'comma-separated list of modules to include'), - ('packages=', 'p', - 'comma-separated list of packages to include'), - ('replace-paths=', None, - 'comma-separated list of paths to replace in included modules'), - ('path=', None, - 'comma-separated list of paths to search'), - ('init-script=', 'i', - 'name of script to use during initialization'), - ('base=', None, - 'name of base executable to use'), - ('compressed', 'c', - 'create a compressed zipfile'), - ('copy-dependent-files', None, - 'copy all dependent files'), - ('create-shared-zip', None, - 'create a shared zip file containing shared modules'), - ('append-script-to-exe', None, - 'append the script module to the exe'), - ('include-in-shared-zip', None, - 'include the script module in the shared zip file'), - ('icon', None, - 'include the icon along with the frozen executable(s)'), - ('constants=', None, - 'comma-separated list of constants to include'), - ('include-files=', 'f', - 'list of tuples of additional files to include in distribution'), - ('bin-includes', None, - 'list of names of files to include when determining dependencies'), - ('bin-excludes', None, - 'list of names of files to exclude when determining dependencies') - ] - boolean_options = ["compressed", "copy_dependent_files", - "create_shared_zip", "append_script_to_exe", - "include_in_shared_zip"] - - def _normalize(self, attrName): - value = getattr(self, attrName) - if value is None: - normalizedValue = [] - elif isinstance(value, basestring): - normalizedValue = value.split() - else: - normalizedValue = list(value) - setattr(self, attrName, normalizedValue) - - def initialize_options(self): - self.optimize = 0 - self.build_exe = None - self.excludes = [] - self.includes = [] - self.packages = [] - self.replace_paths = [] - self.compressed = None - self.copy_dependent_files = None - self.init_script = None - self.base = None - self.path = None - self.create_shared_zip = None - self.append_script_to_exe = None - self.include_in_shared_zip = None - self.icon = None - self.constants = [] - self.include_files = [] - self.bin_excludes = [] - self.bin_includes = [] - - def finalize_options(self): - self.set_undefined_options('build', ('build_exe', 'build_exe')) - self.optimize = int(self.optimize) - self._normalize("excludes") - self._normalize("includes") - self._normalize("packages") - self._normalize("constants") - - def run(self): - metadata = self.distribution.metadata - constantsModule = cx_Freeze.ConstantsModule(metadata.version) - for constant in self.constants: - parts = constant.split("=") - if len(parts) == 1: - name = constant - value = None - else: - name, stringValue = parts - value = eval(stringValue) - constantsModule.values[name] = value - freezer = cx_Freeze.Freezer(self.distribution.executables, - [constantsModule], self.includes, self.excludes, self.packages, - self.replace_paths, self.compressed, self.optimize, - self.copy_dependent_files, self.init_script, self.base, - self.path, self.create_shared_zip, self.append_script_to_exe, - self.include_in_shared_zip, self.build_exe, icon = self.icon, - includeFiles = self.include_files, - binIncludes = self.bin_includes, - binExcludes = self.bin_excludes) - freezer.Freeze() - - -class install(distutils.command.install.install): - user_options = distutils.command.install.install.user_options + [ - ('install-exe=', None, - 'installation directory for executables') - ] - - def expand_dirs(self): - distutils.command.install.install.expand_dirs(self) - self._expand_attrs(['install_exe']) - - def get_sub_commands(self): - subCommands = distutils.command.install.install.get_sub_commands(self) - if self.distribution.executables: - subCommands.append("install_exe") - return [s for s in subCommands if s != "install_egg_info"] - - def initialize_options(self): - distutils.command.install.install.initialize_options(self) - self.install_exe = None - - def finalize_options(self): - if self.prefix is None and sys.platform == "win32": - import _winreg - key = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, - r"Software\Microsoft\Windows\CurrentVersion") - prefix = str(_winreg.QueryValueEx(key, "ProgramFilesDir")[0]) - metadata = self.distribution.metadata - dirName = "%s-%s" % (metadata.name, metadata.version) - self.prefix = "%s/%s" % (prefix, dirName) - distutils.command.install.install.finalize_options(self) - self.convert_paths('exe') - if self.root is not None: - self.change_roots('exe') - - def select_scheme(self, name): - distutils.command.install.install.select_scheme(self, name) - if self.install_exe is None: - if sys.platform == "win32": - self.install_exe = '$base' - else: - metadata = self.distribution.metadata - dirName = "%s-%s" % (metadata.name, metadata.version) - self.install_exe = '$base/lib/%s' % dirName - - -class install_exe(distutils.core.Command): - description = "install executables built from Python scripts" - user_options = [ - ('install-dir=', 'd', 'directory to install executables to'), - ('build-dir=', 'b', 'build directory (where to install from)'), - ('force', 'f', 'force installation (overwrite existing files)'), - ('skip-build', None, 'skip the build steps') - ] - - def initialize_options(self): - self.install_dir = None - self.force = 0 - self.build_dir = None - self.skip_build = None - - def finalize_options(self): - self.set_undefined_options('build', ('build_exe', 'build_dir')) - self.set_undefined_options('install', - ('install_exe', 'install_dir'), - ('force', 'force'), - ('skip_build', 'skip_build')) - - def run(self): - if not self.skip_build: - self.run_command('build_exe') - self.outfiles = self.copy_tree(self.build_dir, self.install_dir) - if sys.platform != "win32": - baseDir = os.path.dirname(os.path.dirname(self.install_dir)) - binDir = os.path.join(baseDir, "bin") - if not os.path.exists(binDir): - os.makedirs(binDir) - sourceDir = os.path.join("..", self.install_dir[len(baseDir) + 1:]) - for executable in self.distribution.executables: - name = os.path.basename(executable.targetName) - source = os.path.join(sourceDir, name) - target = os.path.join(binDir, name) - if os.path.exists(target): - os.unlink(target) - os.symlink(source, target) - self.outfiles.append(target) - - def get_inputs(self): - return self.distribution.executables or [] - - def get_outputs(self): - return self.outfiles or [] - - -def _AddCommandClass(commandClasses, name, cls): - if name not in commandClasses: - commandClasses[name] = cls - - -def setup(**attrs): - attrs["distclass"] = Distribution - commandClasses = attrs.setdefault("cmdclass", {}) - if sys.platform == "win32": - if sys.version_info[:2] >= (2, 5): - _AddCommandClass(commandClasses, "bdist_msi", cx_Freeze.bdist_msi) - else: - _AddCommandClass(commandClasses, "bdist_rpm", cx_Freeze.bdist_rpm) - _AddCommandClass(commandClasses, "build", build) - _AddCommandClass(commandClasses, "build_exe", build_exe) - _AddCommandClass(commandClasses, "install", install) - _AddCommandClass(commandClasses, "install_exe", install_exe) - distutils.core.setup(**attrs) - diff --git a/setup/installer/cx_Freeze/cx_Freeze/finder.py b/setup/installer/cx_Freeze/cx_Freeze/finder.py deleted file mode 100644 index f815db97be..0000000000 --- a/setup/installer/cx_Freeze/cx_Freeze/finder.py +++ /dev/null @@ -1,455 +0,0 @@ -""" -Base class for finding modules. -""" - -import dis -import imp -import marshal -import new -import opcode -import os -import sys -import zipfile - -import cx_Freeze.hooks - -BUILD_LIST = opcode.opmap["BUILD_LIST"] -INPLACE_ADD = opcode.opmap["INPLACE_ADD"] -LOAD_CONST = opcode.opmap["LOAD_CONST"] -IMPORT_NAME = opcode.opmap["IMPORT_NAME"] -IMPORT_FROM = opcode.opmap["IMPORT_FROM"] -STORE_NAME = opcode.opmap["STORE_NAME"] -STORE_GLOBAL = opcode.opmap["STORE_GLOBAL"] -STORE_OPS = (STORE_NAME, STORE_GLOBAL) - -__all__ = [ "Module", "ModuleFinder" ] - -class ModuleFinder(object): - - def __init__(self, includeFiles, excludes, path, replacePaths): - self.includeFiles = includeFiles - self.excludes = dict.fromkeys(excludes) - self.replacePaths = replacePaths - self.path = path or sys.path - self.modules = [] - self.aliases = {} - self._modules = dict.fromkeys(excludes) - self._builtinModules = dict.fromkeys(sys.builtin_module_names) - self._badModules = {} - self._zipFileEntries = {} - self._zipFiles = {} - cx_Freeze.hooks.initialize(self) - - def _AddModule(self, name): - """Add a module to the list of modules but if one is already found, - then return it instead; this is done so that packages can be - handled properly.""" - module = self._modules.get(name) - if module is None: - module = self._modules[name] = Module(name) - self.modules.append(module) - if name in self._badModules: - del self._badModules[name] - return module - - def _DetermineParent(self, caller): - """Determine the parent to use when searching packages.""" - if caller is not None: - if caller.path is not None: - return caller - return self._GetParentByName(caller.name) - - def _EnsureFromList(self, caller, packageModule, fromList, - deferredImports): - """Ensure that the from list is satisfied. This is only necessary for - package modules. If the caller is the package itself, actually - attempt to import right then since it must be a submodule; otherwise - defer until after all global names are defined in order to avoid - spurious complaints about missing modules.""" - if caller is not packageModule: - deferredImports.append((packageModule, fromList)) - else: - if fromList == ("*",): - fromList = packageModule.allNames - for name in fromList: - if name in packageModule.globalNames: - continue - subModuleName = "%s.%s" % (packageModule.name, name) - self._ImportModule(subModuleName, deferredImports, caller) - - def _FindModule(self, name, path): - try: - return imp.find_module(name, path) - except ImportError: - if not path: - path = [] - for location in path: - if name in self._zipFileEntries: - break - if location in self._zipFiles: - continue - if os.path.isdir(location) or not zipfile.is_zipfile(location): - self._zipFiles[location] = None - continue - zip = zipfile.ZipFile(location) - for archiveName in zip.namelist(): - baseName, ext = os.path.splitext(archiveName) - if ext not in ('.pyc', '.pyo'): - continue - moduleName = ".".join(baseName.split("/")) - if moduleName in self._zipFileEntries: - continue - self._zipFileEntries[moduleName] = (zip, archiveName) - self._zipFiles[location] = None - info = self._zipFileEntries.get(name) - if info is not None: - zip, archiveName = info - fp = zip.read(archiveName) - info = (".pyc", "rb", imp.PY_COMPILED) - return fp, os.path.join(zip.filename, archiveName), info - raise - - def _GetParentByName(self, name): - """Return the parent module given the name of a module.""" - pos = name.rfind(".") - if pos > 0: - parentName = name[:pos] - return self._modules[parentName] - - def _ImportAllSubModules(self, module, deferredImports, recursive = True): - """Import all sub modules to the given package.""" - suffixes = dict.fromkeys([s[0] for s in imp.get_suffixes()]) - for dir in module.path: - try: - fileNames = os.listdir(dir) - except os.error: - continue - for fileName in fileNames: - name, ext = os.path.splitext(fileName) - if ext not in suffixes: - continue - if name == "__init__": - continue - subModuleName = "%s.%s" % (module.name, name) - subModule, returnError = \ - self._InternalImportModule(subModuleName, - deferredImports) - if returnError and subModule is None: - raise ImportError, "No module named %s" % subModuleName - module.globalNames[name] = None - if subModule.path and recursive: - self._ImportAllSubModules(subModule, deferredImports, - recursive) - - def _ImportDeferredImports(self, deferredImports): - """Import any sub modules that were deferred, if applicable.""" - while deferredImports: - newDeferredImports = [] - for packageModule, subModuleNames in deferredImports: - self._EnsureFromList(packageModule, packageModule, - subModuleNames, newDeferredImports) - deferredImports = newDeferredImports - - def _ImportModule(self, name, deferredImports, caller = None, - relativeImportIndex = 0): - """Attempt to find the named module and return it or None if no module - by that name could be found.""" - - # absolute import (available in Python 2.5 and up) - # the name given is the only name that will be searched - if relativeImportIndex == 0: - module, returnError = self._InternalImportModule(name, - deferredImports) - - # old style relative import (only possibility in Python 2.4 and prior) - # the name given is tried in all parents until a match is found and if - # no match is found, the global namespace is searched - elif relativeImportIndex < 0: - parent = self._DetermineParent(caller) - while parent is not None: - fullName = "%s.%s" % (parent.name, name) - module, returnError = self._InternalImportModule(fullName, - deferredImports) - if module is not None: - parent.globalNames[name] = None - return module - parent = self._GetParentByName(parent.name) - module, returnError = self._InternalImportModule(name, - deferredImports) - - # new style relative import (available in Python 2.5 and up) - # the index indicates how many levels to traverse and only that level - # is searched for the named module - elif relativeImportIndex > 0: - parent = caller - if parent.path is not None: - relativeImportIndex -= 1 - while parent is not None and relativeImportIndex > 0: - parent = self._GetParentByName(parent.name) - relativeImportIndex -= 1 - if parent is None: - module = None - returnError = True - elif not name: - module = parent - else: - name = "%s.%s" % (parent.name, name) - module, returnError = self._InternalImportModule(name, - deferredImports) - - # if module not found, track that fact - if module is None: - if caller is None: - raise ImportError, "No module named %s" % name - self._RunHook("missing", name, caller) - if returnError and name not in caller.ignoreNames: - callers = self._badModules.setdefault(name, {}) - callers[caller.name] = None - - return module - - def _InternalImportModule(self, name, deferredImports): - """Internal method used for importing a module which assumes that the - name given is an absolute name. None is returned if the module - cannot be found.""" - try: - return self._modules[name], False - except KeyError: - pass - if name in self._builtinModules: - module = self._AddModule(name) - self._RunHook("load", module.name, module) - return module, False - pos = name.rfind(".") - if pos < 0: - path = self.path - searchName = name - parentModule = None - else: - parentName = name[:pos] - parentModule, returnError = \ - self._InternalImportModule(parentName, deferredImports) - if parentModule is None: - return None, returnError - path = parentModule.path - searchName = name[pos + 1:] - if name in self.aliases: - actualName = self.aliases[name] - module, returnError = \ - self._InternalImportModule(actualName, deferredImports) - self._modules[name] = module - return module, returnError - try: - fp, path, info = self._FindModule(searchName, path) - except ImportError: - self._modules[name] = None - return None, True - module = self._LoadModule(name, fp, path, info, deferredImports, - parentModule) - return module, False - - def _LoadModule(self, name, fp, path, info, deferredImports, - parent = None): - """Load the module, given the information acquired by the finder.""" - suffix, mode, type = info - if type == imp.PKG_DIRECTORY: - return self._LoadPackage(name, path, parent, deferredImports) - module = self._AddModule(name) - module.file = path - module.parent = parent - if type == imp.PY_SOURCE: - module.code = compile(fp.read() + "\n", path, "exec") - elif type == imp.PY_COMPILED: - if isinstance(fp, str): - magic = fp[:4] - else: - magic = fp.read(4) - if magic != imp.get_magic(): - raise ImportError, "Bad magic number in %s" % path - if isinstance(fp, str): - module.code = marshal.loads(fp[8:]) - module.inZipFile = True - else: - fp.read(4) - module.code = marshal.load(fp) - self._RunHook("load", module.name, module) - if module.code is not None: - if self.replacePaths: - topLevelModule = module - while topLevelModule.parent is not None: - topLevelModule = topLevelModule.parent - module.code = self._ReplacePathsInCode(topLevelModule, - module.code) - self._ScanCode(module.code, module, deferredImports) - return module - - def _LoadPackage(self, name, path, parent, deferredImports): - """Load the package, given its name and path.""" - module = self._AddModule(name) - module.path = [path] - fp, path, info = imp.find_module("__init__", module.path) - self._LoadModule(name, fp, path, info, deferredImports, parent) - return module - - def _ReplacePathsInCode(self, topLevelModule, co): - """Replace paths in the code as directed, returning a new code object - with the modified paths in place.""" - origFileName = newFileName = os.path.normpath(co.co_filename) - for searchValue, replaceValue in self.replacePaths: - if searchValue == "*": - searchValue = os.path.dirname(topLevelModule.file) - if topLevelModule.path: - searchValue = os.path.dirname(searchValue) - if searchValue: - searchValue = searchValue + os.pathsep - elif not origFileName.startswith(searchValue): - continue - newFileName = replaceValue + origFileName[len(searchValue):] - break - constants = list(co.co_consts) - for i, value in enumerate(constants): - if isinstance(value, type(co)): - constants[i] = self._ReplacePathsInCode(topLevelModule, value) - return new.code(co.co_argcount, co.co_nlocals, co.co_stacksize, - co.co_flags, co.co_code, tuple(constants), co.co_names, - co.co_varnames, newFileName, co.co_name, co.co_firstlineno, - co.co_lnotab, co.co_freevars, co.co_cellvars) - - def _RunHook(self, hookName, moduleName, *args): - """Run hook for the given module if one is present.""" - name = "%s_%s" % (hookName, moduleName.replace(".", "_")) - method = getattr(cx_Freeze.hooks, name, None) - if method is not None: - method(self, *args) - - def _ScanCode(self, co, module, deferredImports): - """Scan code, looking for imported modules and keeping track of the - constants that have been created in order to better tell which - modules are truly missing.""" - opIndex = 0 - arguments = [] - code = co.co_code - numOps = len(code) - while opIndex < numOps: - op = ord(code[opIndex]) - opIndex += 1 - if op >= dis.HAVE_ARGUMENT: - opArg = ord(code[opIndex]) + ord(code[opIndex + 1]) * 256 - opIndex += 2 - if op == LOAD_CONST: - arguments.append(co.co_consts[opArg]) - elif op == IMPORT_NAME: - name = co.co_names[opArg] - if len(arguments) == 2: - relativeImportIndex, fromList = arguments - else: - relativeImportIndex = -1 - fromList, = arguments - if name not in module.excludeNames: - subModule = self._ImportModule(name, deferredImports, - module, relativeImportIndex) - if subModule is not None: - module.globalNames.update(subModule.globalNames) - if fromList and subModule.path is not None: - self._EnsureFromList(module, subModule, fromList, - deferredImports) - elif op == IMPORT_FROM: - opIndex += 3 - elif op not in (BUILD_LIST, INPLACE_ADD): - if op in STORE_OPS: - name = co.co_names[opArg] - if name == "__all__": - module.allNames.extend(arguments) - module.globalNames[name] = None - arguments = [] - for constant in co.co_consts: - if isinstance(constant, type(co)): - self._ScanCode(constant, module, deferredImports) - - def AddAlias(self, name, aliasFor): - """Add an alias for a particular module; when an attempt is made to - import a module using the alias name, import the actual name - instead.""" - self.aliases[name] = aliasFor - - def ExcludeModule(self, name): - """Exclude the named module from the resulting frozen executable.""" - self.excludes[name] = None - self._modules[name] = None - - def IncludeFile(self, path, moduleName = None): - """Include the named file as a module in the frozen executable.""" - name, ext = os.path.splitext(os.path.basename(path)) - if moduleName is None: - moduleName = name - info = (ext, "r", imp.PY_SOURCE) - deferredImports = [] - module = self._LoadModule(moduleName, file(path, "U"), path, info, - deferredImports) - self._ImportDeferredImports(deferredImports) - return module - - def IncludeFiles(self, sourcePath, targetPath): - """Include the files in the given directory in the target build.""" - self.includeFiles.append((sourcePath, targetPath)) - - def IncludeModule(self, name): - """Include the named module in the frozen executable.""" - deferredImports = [] - module = self._ImportModule(name, deferredImports) - self._ImportDeferredImports(deferredImports) - return module - - def IncludePackage(self, name): - """Include the named package and any submodules in the frozen - executable.""" - deferredImports = [] - module = self._ImportModule(name, deferredImports) - if module.path: - self._ImportAllSubModules(module, deferredImports) - self._ImportDeferredImports(deferredImports) - return module - - def ReportMissingModules(self): - if self._badModules: - print "Missing modules:" - names = self._badModules.keys() - names.sort() - for name in names: - callers = self._badModules[name].keys() - callers.sort() - print "?", name, "imported from", ", ".join(callers) - print - - -class Module(object): - - def __init__(self, name): - self.name = name - self.file = None - self.path = None - self.code = None - self.parent = None - self.globalNames = {} - self.excludeNames = {} - self.ignoreNames = {} - self.allNames = [] - self.inZipFile = False - - def __repr__(self): - parts = ["name=%s" % repr(self.name)] - if self.file is not None: - parts.append("file=%s" % repr(self.file)) - if self.path is not None: - parts.append("path=%s" % repr(self.path)) - return "" % ", ".join(parts) - - def AddGlobalName(self, name): - self.globalNames[name] = None - - def ExcludeName(self, name): - self.excludeNames[name] = None - - def IgnoreName(self, name): - self.ignoreNames[name] = None - diff --git a/setup/installer/cx_Freeze/cx_Freeze/freezer.py b/setup/installer/cx_Freeze/cx_Freeze/freezer.py deleted file mode 100644 index e0739e8a4e..0000000000 --- a/setup/installer/cx_Freeze/cx_Freeze/freezer.py +++ /dev/null @@ -1,550 +0,0 @@ -""" -Base class for freezing scripts into executables. -""" - -import datetime -import distutils.sysconfig -import imp -import marshal -import os -import shutil -import socket -import stat -import struct -import sys -import time -import zipfile - -import cx_Freeze -import cx_Freeze.util - -__all__ = [ "ConfigError", "ConstantsModule", "Executable", "Freezer" ] - -if sys.platform == "win32": - pythonDll = "python%s%s.dll" % sys.version_info[:2] - GLOBAL_BIN_PATH_EXCLUDES = [cx_Freeze.util.GetSystemDir()] - GLOBAL_BIN_INCLUDES = [ - pythonDll, - "gdiplus.dll", - "mfc71.dll", - "msvcp71.dll", - "msvcr71.dll" - ] - GLOBAL_BIN_EXCLUDES = [ - "comctl32.dll", - "oci.dll", - "cx_Logging.pyd" - ] -else: - extension = distutils.sysconfig.get_config_var("SO") - pythonSharedLib = "libpython%s.%s%s" % \ - (sys.version_info[:2] + (extension,)) - GLOBAL_BIN_INCLUDES = [pythonSharedLib] - GLOBAL_BIN_EXCLUDES = [ - "libclntsh.so", - "libwtc9.so" - ] - GLOBAL_BIN_PATH_EXCLUDES = ["/lib", "/lib32", "/lib64", "/usr/lib", - "/usr/lib32", "/usr/lib64"] - - -# NOTE: the try: except: block in this code is not necessary under Python 2.4 -# and higher and can be removed once support for Python 2.3 is no longer needed -EXTENSION_LOADER_SOURCE = \ -""" -import imp, os, sys - -found = False -for p in sys.path: - if not os.path.isdir(p): - continue - f = os.path.join(p, "%s") - if not os.path.exists(f): - continue - try: - m = imp.load_dynamic(__name__, f) - except ImportError: - del sys.modules[__name__] - raise - sys.modules[__name__] = m - found = True - break -if not found: - del sys.modules[__name__] - raise ImportError, "No module named %%s" %% __name__ -""" - - -class Freezer(object): - - def __init__(self, executables, constantsModules = [], includes = [], - excludes = [], packages = [], replacePaths = [], compress = None, - optimizeFlag = 0, copyDependentFiles = None, initScript = None, - base = None, path = None, createLibraryZip = None, - appendScriptToExe = None, appendScriptToLibrary = None, - targetDir = None, binIncludes = [], binExcludes = [], - binPathIncludes = [], binPathExcludes = [], icon = None, - includeFiles = []): - self.executables = executables - self.constantsModules = constantsModules - self.includes = includes - self.excludes = excludes - self.packages = packages - self.replacePaths = replacePaths - self.compress = compress - self.optimizeFlag = optimizeFlag - self.copyDependentFiles = copyDependentFiles - self.initScript = initScript - self.base = base - self.path = path - self.createLibraryZip = createLibraryZip - self.appendScriptToExe = appendScriptToExe - self.appendScriptToLibrary = appendScriptToLibrary - self.targetDir = targetDir - self.binIncludes = [os.path.normcase(n) \ - for n in GLOBAL_BIN_INCLUDES + binIncludes] - self.binExcludes = [os.path.normcase(n) \ - for n in GLOBAL_BIN_EXCLUDES + binExcludes] - self.binPathIncludes = [os.path.normcase(n) for n in binPathIncludes] - self.binPathExcludes = [os.path.normcase(n) \ - for n in GLOBAL_BIN_PATH_EXCLUDES + binPathExcludes] - self.icon = icon - self.includeFiles = includeFiles - self._VerifyConfiguration() - - def _CopyFile(self, source, target, copyDependentFiles, - includeMode = False): - normalizedSource = os.path.normcase(os.path.normpath(source)) - normalizedTarget = os.path.normcase(os.path.normpath(target)) - if normalizedTarget in self.filesCopied: - return - if normalizedSource == normalizedTarget: - return - self._RemoveFile(target) - targetDir = os.path.dirname(target) - self._CreateDirectory(targetDir) - print "copying", source, "->", target - shutil.copyfile(source, target) - if includeMode: - shutil.copymode(source, target) - self.filesCopied[normalizedTarget] = None - if copyDependentFiles: - for source in self._GetDependentFiles(source): - target = os.path.join(targetDir, os.path.basename(source)) - self._CopyFile(source, target, copyDependentFiles) - - def _CreateDirectory(self, path): - if not os.path.isdir(path): - print "creating directory", path - os.makedirs(path) - - def _FreezeExecutable(self, exe): - if self.createLibraryZip: - finder = self.finder - else: - finder = self._GetModuleFinder(exe) - if exe.script is None: - scriptModule = None - else: - scriptModule = finder.IncludeFile(exe.script, exe.moduleName) - self._CopyFile(exe.base, exe.targetName, exe.copyDependentFiles, - includeMode = True) - if exe.icon is not None: - if sys.platform == "win32": - cx_Freeze.util.AddIcon(exe.targetName, exe.icon) - else: - targetName = os.path.join(os.path.dirname(exe.targetName), - os.path.basename(exe.icon)) - self._CopyFile(exe.icon, targetName, - copyDependentFiles = False) - if not os.access(exe.targetName, os.W_OK): - mode = os.stat(exe.targetName).st_mode - os.chmod(exe.targetName, mode | stat.S_IWUSR) - if not exe.appendScriptToLibrary: - if exe.appendScriptToExe: - fileName = exe.targetName - else: - baseFileName, ext = os.path.splitext(exe.targetName) - fileName = baseFileName + ".zip" - self._RemoveFile(fileName) - if not self.createLibraryZip and exe.copyDependentFiles: - scriptModule = None - self._WriteModules(fileName, exe.initScript, finder, exe.compress, - exe.copyDependentFiles, scriptModule) - - def _GetBaseFileName(self, argsSource = None): - if argsSource is None: - argsSource = self - name = argsSource.base - if name is None: - if argsSource.copyDependentFiles: - name = "Console" - else: - name = "ConsoleKeepPath" - argsSource.base = self._GetFileName("bases", name) - if argsSource.base is None: - raise ConfigError("no base named %s", name) - - def _GetDependentFiles(self, path): - dependentFiles = self.dependentFiles.get(path) - if dependentFiles is None: - if sys.platform == "win32": - origPath = os.environ["PATH"] - os.environ["PATH"] = origPath + os.pathsep + \ - os.pathsep.join(sys.path) - dependentFiles = cx_Freeze.util.GetDependentFiles(path) - os.environ["PATH"] = origPath - else: - dependentFiles = [] - for line in os.popen('ldd "%s"' % path): - parts = line.strip().split(" => ") - if len(parts) != 2: - continue - dependentFile = parts[1] - if dependentFile == "not found": - print "WARNING: cannot find", parts[0] - continue - pos = dependentFile.find(" (") - if pos >= 0: - dependentFile = dependentFile[:pos].strip() - if dependentFile: - dependentFiles.append(dependentFile) - dependentFiles = self.dependentFiles[path] = \ - [f for f in dependentFiles if self._ShouldCopyFile(f)] - return dependentFiles - - def _GetFileName(self, dir, name): - if os.path.isabs(name): - return name - name = os.path.normcase(name) - fullDir = os.path.join(os.path.dirname(cx_Freeze.__file__), dir) - if os.path.isdir(fullDir): - for fileName in os.listdir(fullDir): - if name == os.path.splitext(os.path.normcase(fileName))[0]: - return os.path.join(fullDir, fileName) - - def _GetInitScriptFileName(self, argsSource = None): - if argsSource is None: - argsSource = self - name = argsSource.initScript - if name is None: - if argsSource.copyDependentFiles: - name = "Console" - else: - name = "ConsoleKeepPath" - argsSource.initScript = self._GetFileName("initscripts", name) - if argsSource.initScript is None: - raise ConfigError("no initscript named %s", name) - - def _GetModuleFinder(self, argsSource = None): - if argsSource is None: - argsSource = self - finder = cx_Freeze.ModuleFinder(self.includeFiles, argsSource.excludes, - argsSource.path, argsSource.replacePaths) - if argsSource.copyDependentFiles: - finder.IncludeModule("imp") - finder.IncludeModule("os") - finder.IncludeModule("sys") - if argsSource.compress: - finder.IncludeModule("zlib") - for name in argsSource.includes: - finder.IncludeModule(name) - for name in argsSource.packages: - finder.IncludePackage(name) - return finder - - def _PrintReport(self, fileName, modules): - print "writing zip file", fileName - print - print " %-25s %s" % ("Name", "File") - print " %-25s %s" % ("----", "----") - for module in modules: - if module.path: - print "P", - else: - print "m", - print "%-25s" % module.name, module.file or "" - print - - def _RemoveFile(self, path): - if os.path.exists(path): - os.chmod(path, 0777) - os.remove(path) - - def _ShouldCopyFile(self, path): - dir, name = os.path.split(os.path.normcase(path)) - parts = name.split(".") - tweaked = False - while True: - if not parts[-1].isdigit(): - break - parts.pop(-1) - tweaked = True - if tweaked: - name = ".".join(parts) - if name in self.binIncludes: - return True - if name in self.binExcludes: - return False - for path in self.binPathIncludes: - if dir.startswith(path): - return True - for path in self.binPathExcludes: - if dir.startswith(path): - return False - return True - - def _VerifyCanAppendToLibrary(self): - if not self.createLibraryZip: - raise ConfigError("script cannot be appended to library zip if " - "one is not being created") - - def _VerifyConfiguration(self): - if self.compress is None: - self.compress = True - if self.copyDependentFiles is None: - self.copyDependentFiles = True - if self.createLibraryZip is None: - self.createLibraryZip = True - if self.appendScriptToExe is None: - self.appendScriptToExe = False - if self.appendScriptToLibrary is None: - self.appendScriptToLibrary = \ - self.createLibraryZip and not self.appendScriptToExe - if self.targetDir is None: - self.targetDir = os.path.abspath("dist") - self._GetInitScriptFileName() - self._GetBaseFileName() - if self.path is None: - self.path = sys.path - if self.appendScriptToLibrary: - self._VerifyCanAppendToLibrary() - for sourceFileName, targetFileName in self.includeFiles: - if not os.path.exists(sourceFileName): - raise ConfigError("cannot find file/directory named %s", - sourceFileName) - if os.path.isabs(targetFileName): - raise ConfigError("target file/directory cannot be absolute") - for executable in self.executables: - executable._VerifyConfiguration(self) - - def _WriteModules(self, fileName, initScript, finder, compress, - copyDependentFiles, scriptModule = None): - initModule = finder.IncludeFile(initScript, "cx_Freeze__init__") - if scriptModule is None: - for module in self.constantsModules: - module.Create(finder) - modules = [m for m in finder.modules \ - if m.name not in self.excludeModules] - else: - modules = [initModule, scriptModule] - self.excludeModules[initModule.name] = None - self.excludeModules[scriptModule.name] = None - itemsToSort = [(m.name, m) for m in modules] - itemsToSort.sort() - modules = [m for n, m in itemsToSort] - self._PrintReport(fileName, modules) - if scriptModule is None: - finder.ReportMissingModules() - targetDir = os.path.dirname(fileName) - self._CreateDirectory(targetDir) - filesToCopy = [] - if os.path.exists(fileName): - mode = "a" - else: - mode = "w" - outFile = zipfile.PyZipFile(fileName, mode, zipfile.ZIP_DEFLATED) - for module in modules: - if module.code is None and module.file is not None: - fileName = os.path.basename(module.file) - baseFileName, ext = os.path.splitext(fileName) - if baseFileName != module.name and module.name != "zlib": - if "." in module.name: - fileName = module.name + ext - generatedFileName = "ExtensionLoader_%s.py" % \ - module.name.replace(".", "_") - module.code = compile(EXTENSION_LOADER_SOURCE % fileName, - generatedFileName, "exec") - target = os.path.join(targetDir, fileName) - filesToCopy.append((module, target)) - if module.code is None: - continue - fileName = "/".join(module.name.split(".")) - if module.path: - fileName += "/__init__" - if module.file is not None and os.path.exists(module.file): - mtime = os.stat(module.file).st_mtime - else: - mtime = time.time() - zipTime = time.localtime(mtime)[:6] - data = imp.get_magic() + struct.pack("" % self.script - - def _VerifyConfiguration(self, freezer): - if self.path is None: - self.path = freezer.path - if self.targetDir is None: - self.targetDir = freezer.targetDir - if self.includes is None: - self.includes = freezer.includes - if self.excludes is None: - self.excludes = freezer.excludes - if self.packages is None: - self.packages = freezer.packages - if self.replacePaths is None: - self.replacePaths = freezer.replacePaths - if self.compress is None: - self.compress = freezer.compress - if self.copyDependentFiles is None: - self.copyDependentFiles = freezer.copyDependentFiles - if self.appendScriptToExe is None: - self.appendScriptToExe = freezer.appendScriptToExe - if self.appendScriptToLibrary is None: - self.appendScriptToLibrary = freezer.appendScriptToLibrary - if self.initScript is None: - self.initScript = freezer.initScript - else: - freezer._GetInitScriptFileName(self) - if self.base is None: - self.base = freezer.base - else: - freezer._GetBaseFileName(self) - if self.appendScriptToLibrary: - freezer._VerifyCanAppendToLibrary() - if self.icon is None: - self.icon = freezer.icon - if self.script is not None: - name, ext = os.path.splitext(os.path.basename(self.script)) - if self.appendScriptToLibrary: - self.moduleName = "%s__main__" % os.path.normcase(name) - else: - self.moduleName = "__main__" - if self.targetName is None: - baseName, ext = os.path.splitext(self.base) - self.targetName = name + ext - self.targetName = os.path.join(self.targetDir, self.targetName) - - -class ConstantsModule(object): - - def __init__(self, releaseString = None, copyright = None, - moduleName = "BUILD_CONSTANTS", timeFormat = "%B %d, %Y %H:%M:%S"): - self.moduleName = moduleName - self.timeFormat = timeFormat - self.values = {} - self.values["BUILD_RELEASE_STRING"] = releaseString - self.values["BUILD_COPYRIGHT"] = copyright - - def Create(self, finder): - """Create the module which consists of declaration statements for each - of the values.""" - today = datetime.datetime.today() - sourceTimestamp = 0 - for module in finder.modules: - if module.file is None: - continue - if module.inZipFile: - continue - if not os.path.exists(module.file): - raise ConfigError("no file named %s", module.file) - timestamp = os.stat(module.file).st_mtime - sourceTimestamp = max(sourceTimestamp, timestamp) - sourceTimestamp = datetime.datetime.fromtimestamp(sourceTimestamp) - self.values["BUILD_TIMESTAMP"] = today.strftime(self.timeFormat) - self.values["BUILD_HOST"] = socket.gethostname().split(".")[0] - self.values["SOURCE_TIMESTAMP"] = \ - sourceTimestamp.strftime(self.timeFormat) - module = finder._AddModule(self.moduleName) - sourceParts = [] - names = self.values.keys() - names.sort() - for name in names: - value = self.values[name] - sourceParts.append("%s = %r" % (name, value)) - source = "\n".join(sourceParts) - module.code = compile(source, "%s.py" % self.moduleName, "exec") - diff --git a/setup/installer/cx_Freeze/cx_Freeze/hooks.py b/setup/installer/cx_Freeze/cx_Freeze/hooks.py deleted file mode 100644 index edc2f78788..0000000000 --- a/setup/installer/cx_Freeze/cx_Freeze/hooks.py +++ /dev/null @@ -1,281 +0,0 @@ -import os -import sys - -def initialize(finder): - """upon initialization of the finder, this routine is called to set up some - automatic exclusions for various platforms.""" - finder.ExcludeModule("FCNTL") - finder.ExcludeModule("os.path") - if os.name == "nt": - finder.ExcludeModule("fcntl") - finder.ExcludeModule("grp") - finder.ExcludeModule("pwd") - finder.ExcludeModule("termios") - else: - finder.ExcludeModule("_winreg") - finder.ExcludeModule("msilib") - finder.ExcludeModule("msvcrt") - finder.ExcludeModule("nt") - if os.name not in ("os2", "ce"): - finder.ExcludeModule("ntpath") - finder.ExcludeModule("nturl2path") - finder.ExcludeModule("pythoncom") - finder.ExcludeModule("pywintypes") - finder.ExcludeModule("winerror") - finder.ExcludeModule("winsound") - finder.ExcludeModule("win32api") - finder.ExcludeModule("win32con") - finder.ExcludeModule("win32event") - finder.ExcludeModule("win32file") - finder.ExcludeModule("win32pdh") - finder.ExcludeModule("win32pipe") - finder.ExcludeModule("win32process") - finder.ExcludeModule("win32security") - finder.ExcludeModule("win32service") - finder.ExcludeModule("wx.activex") - if os.name != "posix": - finder.ExcludeModule("posix") - if os.name != "mac": - finder.ExcludeModule("Carbon") - finder.ExcludeModule("gestalt") - finder.ExcludeModule("ic") - finder.ExcludeModule("mac") - finder.ExcludeModule("MacOS") - finder.ExcludeModule("macpath") - finder.ExcludeModule("macurl2path") - if os.name != "nt": - finder.ExcludeModule("EasyDialogs") - if os.name != "os2": - finder.ExcludeModule("os2") - finder.ExcludeModule("os2emxpath") - finder.ExcludeModule("_emx_link") - if os.name != "ce": - finder.ExcludeModule("ce") - if os.name != "riscos": - finder.ExcludeModule("riscos") - finder.ExcludeModule("riscosenviron") - finder.ExcludeModule("riscospath") - finder.ExcludeModule("rourl2path") - if sys.platform[:4] != "java": - finder.ExcludeModule("java.lang") - finder.ExcludeModule("org.python.core") - - -def load_cElementTree(finder, module): - """the cElementTree module implicitly loads the elementtree.ElementTree - module; make sure this happens.""" - finder.IncludeModule("elementtree.ElementTree") - - -def load_ceODBC(finder, module): - """the ceODBC module implicitly imports both datetime and decimal; make - sure this happens.""" - finder.IncludeModule("datetime") - finder.IncludeModule("decimal") - - -def load_cx_Oracle(finder, module): - """the cx_Oracle module implicitly imports datetime; make sure this - happens.""" - finder.IncludeModule("datetime") - - -def load_docutils_frontend(finder, module): - """The optik module is the old name for the optparse module; ignore the - module if it cannot be found.""" - module.IgnoreName("optik") - - -def load_dummy_threading(finder, module): - """the dummy_threading module plays games with the name of the threading - module for its own purposes; ignore that here""" - finder.ExcludeModule("_dummy_threading") - - -def load_email(finder, module): - """the email package has a bunch of aliases as the submodule names were - all changed to lowercase in Python 2.5; mimic that here.""" - if sys.version_info[:2] >= (2, 5): - for name in ("Charset", "Encoders", "Errors", "FeedParser", - "Generator", "Header", "Iterators", "Message", "Parser", - "Utils", "base64MIME", "quopriMIME"): - finder.AddAlias("email.%s" % name, "email.%s" % name.lower()) - - -def load_ftplib(finder, module): - """the ftplib module attempts to import the SOCKS module; ignore this - module if it cannot be found""" - module.IgnoreName("SOCKS") - - -def load_matplotlib(finder, module): - """the matplotlib module requires data to be found in mpl-data in the - same directory as the frozen executable so oblige it""" - dir = os.path.join(module.path[0], "mpl-data") - finder.IncludeFiles(dir, "mpl-data") - - -def load_matplotlib_numerix(finder, module): - """the numpy.numerix module loads a number of modules dynamically""" - for name in ("ma", "fft", "linear_algebra", "random_array", "mlab"): - finder.IncludeModule("%s.%s" % (module.name, name)) - - -def load_numpy_linalg(finder, module): - """the numpy.linalg module implicitly loads the lapack_lite module; make - sure this happens""" - finder.IncludeModule("numpy.linalg.lapack_lite") - - -def load_pty(finder, module): - """The sgi module is not needed for this module to function.""" - module.IgnoreName("sgi") - - -def load_pythoncom(finder, module): - """the pythoncom module is actually contained in a DLL but since those - cannot be loaded directly in Python 2.5 and higher a special module is - used to perform that task; simply use that technique directly to - determine the name of the DLL and ensure it is included as a normal - extension; also load the pywintypes module which is implicitly - loaded.""" - import pythoncom - module.file = pythoncom.__file__ - module.code = None - finder.IncludeModule("pywintypes") - - -def load_pywintypes(finder, module): - """the pywintypes module is actually contained in a DLL but since those - cannot be loaded directly in Python 2.5 and higher a special module is - used to perform that task; simply use that technique directly to - determine the name of the DLL and ensure it is included as a normal - extension.""" - import pywintypes - module.file = pywintypes.__file__ - module.code = None - - -def load_PyQt4_Qt(finder, module): - """the PyQt4.Qt module is an extension module which imports a number of - other modules and injects their namespace into its own. It seems a - foolish way of doing things but perhaps there is some hidden advantage - to this technique over pure Python; ignore the absence of some of - the modules since not every installation includes all of them.""" - finder.IncludeModule("PyQt4.QtCore") - finder.IncludeModule("PyQt4.QtGui") - finder.IncludeModule("sip") - for name in ("PyQt4.QtSvg", "PyQt4.Qsci", "PyQt4.QtAssistant", - "PyQt4.QtNetwork", "PyQt4.QtOpenGL", "PyQt4.QtScript", "PyQt4._qt", - "PyQt4.QtSql", "PyQt4.QtSvg", "PyQt4.QtTest", "PyQt4.QtXml"): - try: - finder.IncludeModule(name) - except ImportError: - pass - - -def load_Tkinter(finder, module): - """the Tkinter module has data files that are required to be loaded so - ensure that they are copied into the directory that is expected at - runtime.""" - import Tkinter - import _tkinter - tk = _tkinter.create() - tclDir = os.path.dirname(tk.call("info", "library")) - tclSourceDir = os.path.join(tclDir, "tcl%s" % _tkinter.TCL_VERSION) - tkSourceDir = os.path.join(tclDir, "tk%s" % _tkinter.TK_VERSION) - finder.IncludeFiles(tclSourceDir, "tcl") - finder.IncludeFiles(tkSourceDir, "tk") - - -def load_tempfile(finder, module): - """the tempfile module attempts to load the fcntl and thread modules but - continues if these modules cannot be found; ignore these modules if they - cannot be found.""" - module.IgnoreName("fcntl") - module.IgnoreName("thread") - - -def load_time(finder, module): - """the time module implicitly loads _strptime; make sure this happens.""" - finder.IncludeModule("_strptime") - - -def load_win32api(finder, module): - """the win32api module implicitly loads the pywintypes module; make sure - this happens.""" - finder.IncludeModule("pywintypes") - - -def load_win32com(finder, module): - """the win32com package manipulates its search path at runtime to include - the sibling directory called win32comext; simulate that by changing the - search path in a similar fashion here.""" - baseDir = os.path.dirname(os.path.dirname(module.file)) - module.path.append(os.path.join(baseDir, "win32comext")) - - -def load_win32file(finder, module): - """the win32api module implicitly loads the pywintypes module; make sure - this happens.""" - finder.IncludeModule("pywintypes") - - -def load_xml(finder, module): - """the builtin xml package attempts to load the _xmlplus module to see if - that module should take its role instead; ignore the failure to find - this module, though.""" - module.IgnoreName("_xmlplus") - - -def load_xml_etree_cElementTree(finder, module): - """the xml.etree.cElementTree module implicitly loads the - xml.etree.ElementTree module; make sure this happens.""" - finder.IncludeModule("xml.etree.ElementTree") - -def load_IPython(finder, module): - ipy = os.path.join(os.path.dirname(module.file), 'Extensions') - extensions = set([]) - for m in os.listdir(ipy): - extensions.add(os.path.splitext(m)[0]) - extensions.remove('__init__') - for m in extensions: - finder.IncludeModule('IPython.Extensions.'+m) - -def load_lxml(finder, module): - finder.IncludeModule('lxml._elementpath') - -def load_cherrypy(finder, module): - finder.IncludeModule('cherrypy.lib.encoding') - -def missing_cElementTree(finder, caller): - """the cElementTree has been incorporated into the standard library in - Python 2.5 so ignore its absence if it cannot found.""" - if sys.version_info[:2] >= (2, 5): - caller.IgnoreName("cElementTree") - - -def missing_EasyDialogs(finder, caller): - """the EasyDialogs module is not normally present on Windows but it also - may be so instead of excluding it completely, ignore it if it can't be - found""" - if sys.platform == "win32": - caller.IgnoreName("EasyDialogs") - - -def missing_readline(finder, caller): - """the readline module is not normally present on Windows but it also may - be so instead of excluding it completely, ignore it if it can't be - found""" - if sys.platform == "win32": - caller.IgnoreName("readline") - - -def missing_xml_etree(finder, caller): - """the xml.etree package is new for Python 2.5 but it is common practice - to use a try..except.. block in order to support versions earlier than - Python 2.5 transparently; ignore the absence of the package in this - situation.""" - if sys.version_info[:2] < (2, 5): - caller.IgnoreName("xml.etree") - diff --git a/setup/installer/cx_Freeze/cx_Freeze/main.py b/setup/installer/cx_Freeze/cx_Freeze/main.py deleted file mode 100644 index 1704c598d2..0000000000 --- a/setup/installer/cx_Freeze/cx_Freeze/main.py +++ /dev/null @@ -1,171 +0,0 @@ -import optparse -import os -import shutil -import stat -import sys - -import cx_Freeze - -__all__ = ["main"] - -USAGE = \ -""" -%prog [options] [SCRIPT] - -Freeze a Python script and all of its referenced modules to a base -executable which can then be distributed without requiring a Python -installation.""" - -VERSION = \ -""" -%%prog %s -Copyright (c) 2007-2008 Colt Engineering. All rights reserved. -Copyright (c) 2001-2006 Computronix Corporation. All rights reserved.""" % \ - cx_Freeze.version - - -def ParseCommandLine(): - parser = optparse.OptionParser(version = VERSION.strip(), - usage = USAGE.strip()) - parser.add_option("-O", - action = "count", - default = 0, - dest = "optimized", - help = "optimize generated bytecode as per PYTHONOPTIMIZE; " - "use -OO in order to remove doc strings") - parser.add_option("-c", "--compress", - action = "store_true", - dest = "compress", - help = "compress byte code in zip files") - parser.add_option("--base-name", - dest = "baseName", - metavar = "NAME", - help = "file on which to base the target file; if the name of the " - "file is not an absolute file name, the subdirectory bases " - "(rooted in the directory in which the freezer is found) " - "will be searched for a file matching the name") - parser.add_option("--init-script", - dest = "initScript", - metavar = "NAME", - help = "script which will be executed upon startup; if the name " - "of the file is not an absolute file name, the " - "subdirectory initscripts (rooted in the directory in " - "which the cx_Freeze package is found) will be searched " - "for a file matching the name") - parser.add_option("--target-dir", "--install-dir", - dest = "targetDir", - metavar = "DIR", - help = "the directory in which to place the target file and " - "any dependent files") - parser.add_option("--target-name", - dest = "targetName", - metavar = "NAME", - help = "the name of the file to create instead of the base name " - "of the script and the extension of the base binary") - parser.add_option("--no-copy-deps", - dest = "copyDeps", - default = True, - action = "store_false", - help = "do not copy the dependent files (extensions, shared " - "libraries, etc.) to the target directory; this also " - "modifies the default init script to ConsoleKeepPath.py " - "and means that the target executable requires a Python " - "installation to execute properly") - parser.add_option("--default-path", - action = "append", - dest = "defaultPath", - metavar = "DIRS", - help = "list of paths separated by the standard path separator " - "for the platform which will be used to initialize " - "sys.path prior to running the module finder") - parser.add_option("--include-path", - action = "append", - dest = "includePath", - metavar = "DIRS", - help = "list of paths separated by the standard path separator " - "for the platform which will be used to modify sys.path " - "prior to running the module finder") - parser.add_option("--replace-paths", - dest = "replacePaths", - metavar = "DIRECTIVES", - help = "replace all the paths in modules found in the given paths " - "with the given replacement string; multiple values are " - "separated by the standard path separator and each value " - "is of the form path=replacement_string; path can be * " - "which means all paths not already specified") - parser.add_option("--include-modules", - dest = "includeModules", - metavar = "NAMES", - help = "comma separated list of modules to include") - parser.add_option("--exclude-modules", - dest = "excludeModules", - metavar = "NAMES", - help = "comma separated list of modules to exclude") - parser.add_option("--ext-list-file", - dest = "extListFile", - metavar = "NAME", - help = "name of file in which to place the list of dependent files " - "which were copied into the target directory") - parser.add_option("-z", "--zip-include", - dest = "zipIncludes", - action = "append", - default = [], - metavar = "SPEC", - help = "name of file to add to the zip file or a specification of " - "the form name=arcname which will specify the archive name " - "to use; multiple --zip-include arguments can be used") - options, args = parser.parse_args() - if len(args) == 0: - options.script = None - elif len(args) == 1: - options.script, = args - else: - parser.error("only one script can be specified") - if not args and options.includeModules is None and options.copyDeps: - parser.error("script or a list of modules must be specified") - if not args and options.targetName is None: - parser.error("script or a target name must be specified") - if options.excludeModules: - options.excludeModules = options.excludeModules.split(",") - else: - options.excludeModules = [] - if options.includeModules: - options.includeModules = options.includeModules.split(",") - else: - options.includeModules = [] - replacePaths = [] - if options.replacePaths: - for directive in options.replacePaths.split(os.pathsep): - fromPath, replacement = directive.split("=") - replacePaths.append((fromPath, replacement)) - options.replacePaths = replacePaths - if options.defaultPath is not None: - sys.path = [p for mp in options.defaultPath \ - for p in mp.split(os.pathsep)] - if options.includePath is not None: - paths = [p for mp in options.includePath for p in mp.split(os.pathsep)] - sys.path = paths + sys.path - if options.script is not None: - sys.path.insert(0, os.path.dirname(options.script)) - return options - - -def main(): - options = ParseCommandLine() - executables = [cx_Freeze.Executable(options.script, - targetName = options.targetName)] - freezer = cx_Freeze.Freezer(executables, - includes = options.includeModules, - excludes = options.excludeModules, - replacePaths = options.replacePaths, - compress = options.compress, - optimizeFlag = options.optimized, - copyDependentFiles = options.copyDeps, - initScript = options.initScript, - base = options.baseName, - path = None, - createLibraryZip = False, - appendScriptToExe = True, - targetDir = options.targetDir) - freezer.Freeze() - diff --git a/setup/installer/cx_Freeze/cx_Freeze/windist.py b/setup/installer/cx_Freeze/cx_Freeze/windist.py deleted file mode 100644 index 51af544771..0000000000 --- a/setup/installer/cx_Freeze/cx_Freeze/windist.py +++ /dev/null @@ -1,337 +0,0 @@ -import distutils.command.bdist_msi -import msilib -import os - -__all__ = [ "bdist_msi" ] - -# force the remove existing products action to happen first since Windows -# installer appears to be braindead and doesn't handle files shared between -# different "products" very well -sequence = msilib.sequence.InstallExecuteSequence -for index, info in enumerate(sequence): - if info[0] == u'RemoveExistingProducts': - sequence[index] = (info[0], info[1], 1450) - - -class bdist_msi(distutils.command.bdist_msi.bdist_msi): - user_options = distutils.command.bdist_msi.bdist_msi.user_options + [ - ('add-to-path=', None, 'add target dir to PATH environment variable'), - ('upgrade-code=', None, 'upgrade code to use') - ] - x = y = 50 - width = 370 - height = 300 - title = "[ProductName] Setup" - modeless = 1 - modal = 3 - - def add_config(self, fullname): - initialTargetDir = self.get_initial_target_dir(fullname) - if self.add_to_path is None: - self.add_to_path = False - for executable in self.distribution.executables: - if os.path.basename(executable.base).startswith("Console"): - self.add_to_path = True - break - if self.add_to_path: - msilib.add_data(self.db, 'Environment', - [("E_PATH", "Path", r"[~];[TARGETDIR]", "TARGETDIR")]) - msilib.add_data(self.db, 'CustomAction', - [("InitialTargetDir", 256 + 51, "TARGETDIR", initialTargetDir) - ]) - msilib.add_data(self.db, 'InstallExecuteSequence', - [("InitialTargetDir", 'TARGETDIR=""', 401)]) - msilib.add_data(self.db, 'InstallUISequence', - [("PrepareDlg", None, 140), - ("InitialTargetDir", 'TARGETDIR=""', 401), - ("SelectDirectoryDlg", "not Installed", 1230), - ("MaintenanceTypeDlg", - "Installed and not Resume and not Preselected", 1250), - ("ProgressDlg", None, 1280) - ]) - - def add_cancel_dialog(self): - dialog = msilib.Dialog(self.db, "CancelDlg", 50, 10, 260, 85, 3, - self.title, "No", "No", "No") - dialog.text("Text", 48, 15, 194, 30, 3, - "Are you sure you want to cancel [ProductName] installation?") - button = dialog.pushbutton("Yes", 72, 57, 56, 17, 3, "Yes", "No") - button.event("EndDialog", "Exit") - button = dialog.pushbutton("No", 132, 57, 56, 17, 3, "No", "Yes") - button.event("EndDialog", "Return") - - def add_error_dialog(self): - dialog = msilib.Dialog(self.db, "ErrorDlg", 50, 10, 330, 101, 65543, - self.title, "ErrorText", None, None) - dialog.text("ErrorText", 50, 9, 280, 48, 3, "") - for text, x in [("No", 120), ("Yes", 240), ("Abort", 0), - ("Cancel", 42), ("Ignore", 81), ("Ok", 159), ("Retry", 198)]: - button = dialog.pushbutton(text[0], x, 72, 81, 21, 3, text, None) - button.event("EndDialog", "Error%s" % text) - - def add_exit_dialog(self): - dialog = distutils.command.bdist_msi.PyDialog(self.db, "ExitDialog", - self.x, self.y, self.width, self.height, self.modal, - self.title, "Finish", "Finish", "Finish") - dialog.title("Completing the [ProductName] installer") - dialog.back("< Back", "Finish", active = False) - dialog.cancel("Cancel", "Back", active = False) - dialog.text("Description", 15, 235, 320, 20, 0x30003, - "Click the Finish button to exit the installer.") - button = dialog.next("Finish", "Cancel", name = "Finish") - button.event("EndDialog", "Return") - - def add_fatal_error_dialog(self): - dialog = distutils.command.bdist_msi.PyDialog(self.db, "FatalError", - self.x, self.y, self.width, self.height, self.modal, - self.title, "Finish", "Finish", "Finish") - dialog.title("[ProductName] installer ended prematurely") - dialog.back("< Back", "Finish", active = False) - dialog.cancel("Cancel", "Back", active = False) - dialog.text("Description1", 15, 70, 320, 80, 0x30003, - "[ProductName] setup ended prematurely because of an error. " - "Your system has not been modified. To install this program " - "at a later time, please run the installation again.") - dialog.text("Description2", 15, 155, 320, 20, 0x30003, - "Click the Finish button to exit the installer.") - button = dialog.next("Finish", "Cancel", name = "Finish") - button.event("EndDialog", "Exit") - - def add_files_in_use_dialog(self): - dialog = distutils.command.bdist_msi.PyDialog(self.db, "FilesInUse", - self.x, self.y, self.width, self.height, 19, self.title, - "Retry", "Retry", "Retry", bitmap = False) - dialog.text("Title", 15, 6, 200, 15, 0x30003, - r"{\DlgFontBold8}Files in Use") - dialog.text("Description", 20, 23, 280, 20, 0x30003, - "Some files that need to be updated are currently in use.") - dialog.text("Text", 20, 55, 330, 50, 3, - "The following applications are using files that need to be " - "updated by this setup. Close these applications and then " - "click Retry to continue the installation or Cancel to exit " - "it.") - dialog.control("List", "ListBox", 20, 107, 330, 130, 7, - "FileInUseProcess", None, None, None) - button = dialog.back("Exit", "Ignore", name = "Exit") - button.event("EndDialog", "Exit") - button = dialog.next("Ignore", "Retry", name = "Ignore") - button.event("EndDialog", "Ignore") - button = dialog.cancel("Retry", "Exit", name = "Retry") - button.event("EndDialog", "Retry") - - def add_maintenance_type_dialog(self): - dialog = distutils.command.bdist_msi.PyDialog(self.db, - "MaintenanceTypeDlg", self.x, self.y, self.width, self.height, - self.modal, self.title, "Next", "Next", "Cancel") - dialog.title("Welcome to the [ProductName] Setup Wizard") - dialog.text("BodyText", 15, 63, 330, 42, 3, - "Select whether you want to repair or remove [ProductName].") - group = dialog.radiogroup("RepairRadioGroup", 15, 108, 330, 60, 3, - "MaintenanceForm_Action", "", "Next") - group.add("Repair", 0, 18, 300, 17, "&Repair [ProductName]") - group.add("Remove", 0, 36, 300, 17, "Re&move [ProductName]") - dialog.back("< Back", None, active = False) - button = dialog.next("Finish", "Cancel") - button.event("[REINSTALL]", "ALL", - 'MaintenanceForm_Action="Repair"', 5) - button.event("[Progress1]", "Repairing", - 'MaintenanceForm_Action="Repair"', 6) - button.event("[Progress2]", "repairs", - 'MaintenanceForm_Action="Repair"', 7) - button.event("Reinstall", "ALL", - 'MaintenanceForm_Action="Repair"', 8) - button.event("[REMOVE]", "ALL", - 'MaintenanceForm_Action="Remove"', 11) - button.event("[Progress1]", "Removing", - 'MaintenanceForm_Action="Remove"', 12) - button.event("[Progress2]", "removes", - 'MaintenanceForm_Action="Remove"', 13) - button.event("Remove", "ALL", - 'MaintenanceForm_Action="Remove"', 14) - button.event("EndDialog", "Return", - 'MaintenanceForm_Action<>"Change"', 20) - button = dialog.cancel("Cancel", "RepairRadioGroup") - button.event("SpawnDialog", "CancelDlg") - - def add_prepare_dialog(self): - dialog = distutils.command.bdist_msi.PyDialog(self.db, "PrepareDlg", - self.x, self.y, self.width, self.height, self.modeless, - self.title, "Cancel", "Cancel", "Cancel") - dialog.text("Description", 15, 70, 320, 40, 0x30003, - "Please wait while the installer prepares to guide you through" - "the installation.") - dialog.title("Welcome to the [ProductName] installer") - text = dialog.text("ActionText", 15, 110, 320, 20, 0x30003, - "Pondering...") - text.mapping("ActionText", "Text") - text = dialog.text("ActionData", 15, 135, 320, 30, 0x30003, None) - text.mapping("ActionData", "Text") - dialog.back("Back", None, active = False) - dialog.next("Next", None, active = False) - button = dialog.cancel("Cancel", None) - button.event("SpawnDialog", "CancelDlg") - - def add_progress_dialog(self): - dialog = distutils.command.bdist_msi.PyDialog(self.db, "ProgressDlg", - self.x, self.y, self.width, self.height, self.modeless, - self.title, "Cancel", "Cancel", "Cancel", bitmap = False) - dialog.text("Title", 20, 15, 200, 15, 0x30003, - r"{\DlgFontBold8}[Progress1] [ProductName]") - dialog.text("Text", 35, 65, 300, 30, 3, - "Please wait while the installer [Progress2] [ProductName].") - dialog.text("StatusLabel", 35, 100 ,35, 20, 3, "Status:") - text = dialog.text("ActionText", 70, 100, self.width - 70, 20, 3, - "Pondering...") - text.mapping("ActionText", "Text") - control = dialog.control("ProgressBar", "ProgressBar", 35, 120, 300, - 10, 65537, None, "Progress done", None, None) - control.mapping("SetProgress", "Progress") - dialog.back("< Back", "Next", active = False) - dialog.next("Next >", "Cancel", active = False) - button = dialog.cancel("Cancel", "Back") - button.event("SpawnDialog", "CancelDlg") - - def add_properties(self): - metadata = self.distribution.metadata - props = [ - ('DistVersion', metadata.get_version()), - ('DefaultUIFont', 'DlgFont8'), - ('ErrorDialog', 'ErrorDlg'), - ('Progress1', 'Install'), - ('Progress2', 'installs'), - ('MaintenanceForm_Action', 'Repair') - ] - email = metadata.author_email or metadata.maintainer_email - if email: - props.append(("ARPCONTACT", email)) - if metadata.url: - props.append(("ARPURLINFOABOUT", metadata.url)) - if self.upgrade_code is not None: - props.append(("UpgradeCode", self.upgrade_code)) - msilib.add_data(self.db, 'Property', props) - - def add_select_directory_dialog(self): - dialog = distutils.command.bdist_msi.PyDialog(self.db, - "SelectDirectoryDlg", self.x, self.y, self.width, self.height, - self.modal, self.title, "Next", "Next", "Cancel") - dialog.title("Select destination directory") - dialog.back("< Back", None, active = False) - button = dialog.next("Next >", "Cancel") - button.event("SetTargetPath", "TARGETDIR", ordering = 1) - button.event("SpawnWaitDialog", "WaitForCostingDlg", ordering = 2) - button.event("EndDialog", "Return", ordering = 3) - button = dialog.cancel("Cancel", "DirectoryCombo") - button.event("SpawnDialog", "CancelDlg") - dialog.control("DirectoryCombo", "DirectoryCombo", 15, 70, 272, 80, - 393219, "TARGETDIR", None, "DirectoryList", None) - dialog.control("DirectoryList", "DirectoryList", 15, 90, 308, 136, 3, - "TARGETDIR", None, "PathEdit", None) - dialog.control("PathEdit", "PathEdit", 15, 230, 306, 16, 3, - "TARGETDIR", None, "Next", None) - button = dialog.pushbutton("Up", 306, 70, 18, 18, 3, "Up", None) - button.event("DirectoryListUp", "0") - button = dialog.pushbutton("NewDir", 324, 70, 30, 18, 3, "New", None) - button.event("DirectoryListNew", "0") - - def add_text_styles(self): - msilib.add_data(self.db, 'TextStyle', - [("DlgFont8", "Tahoma", 9, None, 0), - ("DlgFontBold8", "Tahoma", 8, None, 1), - ("VerdanaBold10", "Verdana", 10, None, 1), - ("VerdanaRed9", "Verdana", 9, 255, 0) - ]) - - def add_ui(self): - self.add_text_styles() - self.add_error_dialog() - self.add_fatal_error_dialog() - self.add_cancel_dialog() - self.add_exit_dialog() - self.add_user_exit_dialog() - self.add_files_in_use_dialog() - self.add_wait_for_costing_dialog() - self.add_prepare_dialog() - self.add_select_directory_dialog() - self.add_progress_dialog() - self.add_maintenance_type_dialog() - - def add_upgrade_config(self, sversion): - if self.upgrade_code is not None: - msilib.add_data(self.db, 'Upgrade', - [(self.upgrade_code, None, sversion, None, 513, None, - "REMOVEOLDVERSION"), - (self.upgrade_code, sversion, None, None, 257, None, - "REMOVENEWVERSION") - ]) - - def add_user_exit_dialog(self): - dialog = distutils.command.bdist_msi.PyDialog(self.db, "UserExit", - self.x, self.y, self.width, self.height, self.modal, - self.title, "Finish", "Finish", "Finish") - dialog.title("[ProductName] installer was interrupted") - dialog.back("< Back", "Finish", active = False) - dialog.cancel("Cancel", "Back", active = False) - dialog.text("Description1", 15, 70, 320, 80, 0x30003, - "[ProductName] setup was interrupted. Your system has not " - "been modified. To install this program at a later time, " - "please run the installation again.") - dialog.text("Description2", 15, 155, 320, 20, 0x30003, - "Click the Finish button to exit the installer.") - button = dialog.next("Finish", "Cancel", name = "Finish") - button.event("EndDialog", "Exit") - - def add_wait_for_costing_dialog(self): - dialog = msilib.Dialog(self.db, "WaitForCostingDlg", 50, 10, 260, 85, - self.modal, self.title, "Return", "Return", "Return") - dialog.text("Text", 48, 15, 194, 30, 3, - "Please wait while the installer finishes determining your " - "disk space requirements.") - button = dialog.pushbutton("Return", 102, 57, 56, 17, 3, "Return", - None) - button.event("EndDialog", "Exit") - - def get_initial_target_dir(self, fullname): - return r"[ProgramFilesFolder]\%s" % fullname - - def get_installer_filename(self, fullname): - return os.path.join(self.dist_dir, "%s.msi" % fullname) - - def initialize_options(self): - distutils.command.bdist_msi.bdist_msi.initialize_options(self) - self.upgrade_code = None - self.add_to_path = None - - def run(self): - if not self.skip_build: - self.run_command('build') - install = self.reinitialize_command('install', reinit_subcommands = 1) - install.prefix = self.bdist_dir - install.skip_build = self.skip_build - install.warn_dir = 0 - distutils.log.info("installing to %s", self.bdist_dir) - install.ensure_finalized() - install.run() - self.mkpath(self.dist_dir) - fullname = self.distribution.get_fullname() - filename = os.path.abspath(self.get_installer_filename(fullname)) - if os.path.exists(filename): - os.unlink(filename) - metadata = self.distribution.metadata - author = metadata.author or metadata.maintainer or "UNKNOWN" - version = metadata.get_version() - sversion = "%d.%d.%d" % \ - distutils.version.StrictVersion(version).version - self.db = msilib.init_database(filename, msilib.schema, - self.distribution.metadata.name, msilib.gen_uuid(), sversion, - author) - msilib.add_tables(self.db, msilib.sequence) - self.add_properties() - self.add_config(fullname) - self.add_upgrade_config(sversion) - self.add_ui() - self.add_files() - self.db.Commit() - if not self.keep_temp: - distutils.dir_util.remove_tree(self.bdist_dir, - dry_run = self.dry_run) - diff --git a/setup/installer/cx_Freeze/cxfreeze b/setup/installer/cx_Freeze/cxfreeze deleted file mode 100755 index acd6789833..0000000000 --- a/setup/installer/cx_Freeze/cxfreeze +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/python - -from cx_Freeze import main - -main() - diff --git a/setup/installer/cx_Freeze/initscripts/Console.py b/setup/installer/cx_Freeze/initscripts/Console.py deleted file mode 100755 index e10649d722..0000000000 --- a/setup/installer/cx_Freeze/initscripts/Console.py +++ /dev/null @@ -1,35 +0,0 @@ -#------------------------------------------------------------------------------ -# Console.py -# Initialization script for cx_Freeze which manipulates the path so that the -# directory in which the executable is found is searched for extensions but -# no other directory is searched. It also sets the attribute sys.frozen so that -# the Win32 extensions behave as expected. -#------------------------------------------------------------------------------ - -import encodings -import os -import sys -import warnings -import zipimport - -sys.frozen = True -sys.path = sys.path[:4] - -os.environ["TCL_LIBRARY"] = os.path.join(DIR_NAME, "tcl") -os.environ["TK_LIBRARY"] = os.path.join(DIR_NAME, "tk") - -m = __import__("__main__") -importer = zipimport.zipimporter(INITSCRIPT_ZIP_FILE_NAME) -if INITSCRIPT_ZIP_FILE_NAME != SHARED_ZIP_FILE_NAME: - moduleName = m.__name__ -else: - name, ext = os.path.splitext(os.path.basename(os.path.normcase(FILE_NAME))) - moduleName = "%s__main__" % name -code = importer.get_code(moduleName) -exec code in m.__dict__ - -if sys.version_info[:2] >= (2, 5): - module = sys.modules.get("threading") - if module is not None: - module._shutdown() - diff --git a/setup/installer/cx_Freeze/initscripts/ConsoleKeepPath.py b/setup/installer/cx_Freeze/initscripts/ConsoleKeepPath.py deleted file mode 100755 index 60151a1ff6..0000000000 --- a/setup/installer/cx_Freeze/initscripts/ConsoleKeepPath.py +++ /dev/null @@ -1,19 +0,0 @@ -#------------------------------------------------------------------------------ -# ConsoleKeepPath.py -# Initialization script for cx_Freeze which leaves the path alone and does -# not set the sys.frozen attribute. -#------------------------------------------------------------------------------ - -import sys -import zipimport - -m = __import__("__main__") -importer = zipimport.zipimporter(INITSCRIPT_ZIP_FILE_NAME) -code = importer.get_code(m.__name__) -exec code in m.__dict__ - -if sys.version_info[:2] >= (2, 5): - module = sys.modules.get("threading") - if module is not None: - module._shutdown() - diff --git a/setup/installer/cx_Freeze/initscripts/ConsoleSetLibPath.py b/setup/installer/cx_Freeze/initscripts/ConsoleSetLibPath.py deleted file mode 100755 index b558652c0a..0000000000 --- a/setup/installer/cx_Freeze/initscripts/ConsoleSetLibPath.py +++ /dev/null @@ -1,38 +0,0 @@ -#------------------------------------------------------------------------------ -# ConsoleSetLibPath.py -# Initialization script for cx_Freeze which manipulates the path so that the -# directory in which the executable is found is searched for extensions but -# no other directory is searched. The environment variable LD_LIBRARY_PATH is -# manipulated first, however, to ensure that shared libraries found in the -# target directory are found. This requires a restart of the executable because -# the environment variable LD_LIBRARY_PATH is only checked at startup. -#------------------------------------------------------------------------------ - -import encodings -import os -import sys -import warnings -import zipimport - -paths = os.environ.get("LD_LIBRARY_PATH", "").split(os.pathsep) -if DIR_NAME not in paths: - paths.insert(0, DIR_NAME) - os.environ["LD_LIBRARY_PATH"] = os.pathsep.join(paths) - os.execv(sys.executable, sys.argv) - -sys.frozen = True -sys.path = sys.path[:4] - -os.environ["TCL_LIBRARY"] = os.path.join(DIR_NAME, "tcl") -os.environ["TK_LIBRARY"] = os.path.join(DIR_NAME, "tk") - -m = __import__("__main__") -importer = zipimport.zipimporter(INITSCRIPT_ZIP_FILE_NAME) -code = importer.get_code(m.__name__) -exec code in m.__dict__ - -if sys.version_info[:2] >= (2, 5): - module = sys.modules.get("threading") - if module is not None: - module._shutdown() - diff --git a/setup/installer/cx_Freeze/initscripts/SharedLib.py b/setup/installer/cx_Freeze/initscripts/SharedLib.py deleted file mode 100755 index 0445367010..0000000000 --- a/setup/installer/cx_Freeze/initscripts/SharedLib.py +++ /dev/null @@ -1,20 +0,0 @@ -#------------------------------------------------------------------------------ -# SharedLib.py -# Initialization script for cx_Freeze which behaves similarly to the one for -# console based applications but must handle the case where Python has already -# been initialized and another DLL of this kind has been loaded. As such it -# does not block the path unless sys.frozen is not already set. -#------------------------------------------------------------------------------ - -import encodings -import os -import sys -import warnings - -if not hasattr(sys, "frozen"): - sys.frozen = True - sys.path = sys.path[:4] - -os.environ["TCL_LIBRARY"] = os.path.join(DIR_NAME, "tcl") -os.environ["TK_LIBRARY"] = os.path.join(DIR_NAME, "tk") - diff --git a/setup/installer/cx_Freeze/initscripts/SharedLibSource.py b/setup/installer/cx_Freeze/initscripts/SharedLibSource.py deleted file mode 100755 index 3edae93694..0000000000 --- a/setup/installer/cx_Freeze/initscripts/SharedLibSource.py +++ /dev/null @@ -1,23 +0,0 @@ -#------------------------------------------------------------------------------ -# SharedLibSource.py -# Initialization script for cx_Freeze which imports the site module (as per -# normal processing of a Python script) and then searches for a file with the -# same name as the shared library but with the extension .pth. The entries in -# this file are used to modify the path to use for subsequent imports. -#------------------------------------------------------------------------------ - -import os -import sys -import warnings - -# the site module must be imported for normal behavior to take place; it is -# done dynamically so that cx_Freeze will not add all modules referenced by -# the site module to the frozen executable -__import__("site") - -# now locate the pth file to modify the path appropriately -baseName, ext = os.path.splitext(FILE_NAME) -pathFileName = baseName + ".pth" -sys.path = [s.strip() for s in file(pathFileName).read().splitlines()] + \ - sys.path - diff --git a/setup/installer/cx_Freeze/samples/advanced/advanced_1.py b/setup/installer/cx_Freeze/samples/advanced/advanced_1.py deleted file mode 100644 index 2f1b68bceb..0000000000 --- a/setup/installer/cx_Freeze/samples/advanced/advanced_1.py +++ /dev/null @@ -1,7 +0,0 @@ -import sys - -print "Hello from cx_Freeze Advanced #1" -print - -module = __import__("testfreeze_1") - diff --git a/setup/installer/cx_Freeze/samples/advanced/advanced_2.py b/setup/installer/cx_Freeze/samples/advanced/advanced_2.py deleted file mode 100644 index 1a6fe37e62..0000000000 --- a/setup/installer/cx_Freeze/samples/advanced/advanced_2.py +++ /dev/null @@ -1,7 +0,0 @@ -import sys - -print "Hello from cx_Freeze Advanced #2" -print - -module = __import__("testfreeze_2") - diff --git a/setup/installer/cx_Freeze/samples/advanced/modules/testfreeze_1.py b/setup/installer/cx_Freeze/samples/advanced/modules/testfreeze_1.py deleted file mode 100644 index 6157b72a69..0000000000 --- a/setup/installer/cx_Freeze/samples/advanced/modules/testfreeze_1.py +++ /dev/null @@ -1 +0,0 @@ -print "Test freeze module #1" diff --git a/setup/installer/cx_Freeze/samples/advanced/modules/testfreeze_2.py b/setup/installer/cx_Freeze/samples/advanced/modules/testfreeze_2.py deleted file mode 100644 index ca133a7d58..0000000000 --- a/setup/installer/cx_Freeze/samples/advanced/modules/testfreeze_2.py +++ /dev/null @@ -1 +0,0 @@ -print "Test freeze module #2" diff --git a/setup/installer/cx_Freeze/samples/advanced/setup.py b/setup/installer/cx_Freeze/samples/advanced/setup.py deleted file mode 100644 index 3a79cf23af..0000000000 --- a/setup/installer/cx_Freeze/samples/advanced/setup.py +++ /dev/null @@ -1,31 +0,0 @@ -# An advanced setup script to create multiple executables and demonstrate a few -# of the features available to setup scripts -# -# hello.py is a very simple "Hello, world" type script which also displays the -# environment in which the script runs -# -# Run the build process by running the command 'python setup.py build' -# -# If everything works well you should find a subdirectory in the build -# subdirectory that contains the files needed to run the script without Python - -import sys -from cx_Freeze import setup, Executable - -executables = [ - Executable("advanced_1.py"), - Executable("advanced_2.py") -] - -buildOptions = dict( - compressed = True, - includes = ["testfreeze_1", "testfreeze_2"], - path = sys.path + ["modules"]) - -setup( - name = "advanced_cx_Freeze_sample", - version = "0.1", - description = "Advanced sample cx_Freeze script", - options = dict(build_exe = buildOptions), - executables = executables) - diff --git a/setup/installer/cx_Freeze/samples/matplotlib/setup.py b/setup/installer/cx_Freeze/samples/matplotlib/setup.py deleted file mode 100644 index 54bd97fc3f..0000000000 --- a/setup/installer/cx_Freeze/samples/matplotlib/setup.py +++ /dev/null @@ -1,27 +0,0 @@ -# A simple setup script to create an executable using matplotlib. -# -# test_matplotlib.py is a very simple matplotlib application that demonstrates -# its use. -# -# Run the build process by running the command 'python setup.py build' -# -# If everything works well you should find a subdirectory in the build -# subdirectory that contains the files needed to run the application - -import cx_Freeze -import sys - -base = None -if sys.platform == "win32": - base = "Win32GUI" - -executables = [ - cx_Freeze.Executable("test_matplotlib.py", base = base) -] - -cx_Freeze.setup( - name = "test_matplotlib", - version = "0.1", - description = "Sample matplotlib script", - executables = executables) - diff --git a/setup/installer/cx_Freeze/samples/matplotlib/test_matplotlib.py b/setup/installer/cx_Freeze/samples/matplotlib/test_matplotlib.py deleted file mode 100644 index 2029845ca5..0000000000 --- a/setup/installer/cx_Freeze/samples/matplotlib/test_matplotlib.py +++ /dev/null @@ -1,48 +0,0 @@ -from numpy import arange, sin, pi -import matplotlib -matplotlib.use('WXAgg') -from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas -from matplotlib.backends.backend_wx import NavigationToolbar2Wx -from matplotlib.figure import Figure -from wx import * - -class CanvasFrame(Frame): - def __init__(self): - Frame.__init__(self,None,-1, 'CanvasFrame',size=(550,350)) - self.SetBackgroundColour(NamedColor("WHITE")) - self.figure = Figure() - self.axes = self.figure.add_subplot(111) - t = arange(0.0,3.0,0.01) - s = sin(2*pi*t) - self.axes.plot(t,s) - self.canvas = FigureCanvas(self, -1, self.figure) - self.sizer = BoxSizer(VERTICAL) - self.sizer.Add(self.canvas, 1, LEFT | TOP | GROW) - self.SetSizerAndFit(self.sizer) - self.add_toolbar() - - def add_toolbar(self): - self.toolbar = NavigationToolbar2Wx(self.canvas) - self.toolbar.Realize() - if Platform == '__WXMAC__': - self.SetToolBar(self.toolbar) - else: - tw, th = self.toolbar.GetSizeTuple() - fw, fh = self.canvas.GetSizeTuple() - self.toolbar.SetSize(Size(fw, th)) - self.sizer.Add(self.toolbar, 0, LEFT | EXPAND) - self.toolbar.update() - - def OnPaint(self, event): - self.canvas.draw() - -class App(App): - def OnInit(self): - 'Create the main window and insert the custom frame' - frame = CanvasFrame() - frame.Show(True) - return True - -app = App(0) -app.MainLoop() - diff --git a/setup/installer/cx_Freeze/samples/relimport/pkg1/__init__.py b/setup/installer/cx_Freeze/samples/relimport/pkg1/__init__.py deleted file mode 100644 index 5a170fd2dd..0000000000 --- a/setup/installer/cx_Freeze/samples/relimport/pkg1/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -print "importing pkg1" -from . import sub1 -from . import pkg2 diff --git a/setup/installer/cx_Freeze/samples/relimport/pkg1/pkg2/__init__.py b/setup/installer/cx_Freeze/samples/relimport/pkg1/pkg2/__init__.py deleted file mode 100644 index 71e0b1fbe6..0000000000 --- a/setup/installer/cx_Freeze/samples/relimport/pkg1/pkg2/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -print "importing pkg1.pkg2" -from . import sub3 -from .. import sub4 diff --git a/setup/installer/cx_Freeze/samples/relimport/pkg1/pkg2/sub3.py b/setup/installer/cx_Freeze/samples/relimport/pkg1/pkg2/sub3.py deleted file mode 100644 index 1719aadb41..0000000000 --- a/setup/installer/cx_Freeze/samples/relimport/pkg1/pkg2/sub3.py +++ /dev/null @@ -1,3 +0,0 @@ -print "importing pkg1.pkg2.sub3" -from . import sub5 -from .. import sub6 diff --git a/setup/installer/cx_Freeze/samples/relimport/pkg1/pkg2/sub5.py b/setup/installer/cx_Freeze/samples/relimport/pkg1/pkg2/sub5.py deleted file mode 100644 index 1c91b8fa23..0000000000 --- a/setup/installer/cx_Freeze/samples/relimport/pkg1/pkg2/sub5.py +++ /dev/null @@ -1 +0,0 @@ -print "importing pkg1.pkg2.sub5" diff --git a/setup/installer/cx_Freeze/samples/relimport/pkg1/sub1.py b/setup/installer/cx_Freeze/samples/relimport/pkg1/sub1.py deleted file mode 100644 index 514bd88a87..0000000000 --- a/setup/installer/cx_Freeze/samples/relimport/pkg1/sub1.py +++ /dev/null @@ -1,2 +0,0 @@ -print "importing pkg1.sub1" -from . import sub2 diff --git a/setup/installer/cx_Freeze/samples/relimport/pkg1/sub2.py b/setup/installer/cx_Freeze/samples/relimport/pkg1/sub2.py deleted file mode 100644 index 63a0838b25..0000000000 --- a/setup/installer/cx_Freeze/samples/relimport/pkg1/sub2.py +++ /dev/null @@ -1 +0,0 @@ -print "importing pkg1.sub2" diff --git a/setup/installer/cx_Freeze/samples/relimport/pkg1/sub4.py b/setup/installer/cx_Freeze/samples/relimport/pkg1/sub4.py deleted file mode 100644 index 3a8e760e43..0000000000 --- a/setup/installer/cx_Freeze/samples/relimport/pkg1/sub4.py +++ /dev/null @@ -1 +0,0 @@ -print 'importing pkg1.sub4' diff --git a/setup/installer/cx_Freeze/samples/relimport/pkg1/sub6.py b/setup/installer/cx_Freeze/samples/relimport/pkg1/sub6.py deleted file mode 100644 index 1e7d7955d6..0000000000 --- a/setup/installer/cx_Freeze/samples/relimport/pkg1/sub6.py +++ /dev/null @@ -1 +0,0 @@ -print "importing pkg1.sub6" diff --git a/setup/installer/cx_Freeze/samples/relimport/relimport.py b/setup/installer/cx_Freeze/samples/relimport/relimport.py deleted file mode 100644 index 39cb50c91d..0000000000 --- a/setup/installer/cx_Freeze/samples/relimport/relimport.py +++ /dev/null @@ -1 +0,0 @@ -import pkg1 diff --git a/setup/installer/cx_Freeze/samples/relimport/setup.py b/setup/installer/cx_Freeze/samples/relimport/setup.py deleted file mode 100644 index b8b3f3853b..0000000000 --- a/setup/installer/cx_Freeze/samples/relimport/setup.py +++ /dev/null @@ -1,16 +0,0 @@ -# relimport.py is a very simple script that tests importing using relative -# imports (available in Python 2.5 and up) -# -# Run the build process by running the command 'python setup.py build' -# -# If everything works well you should find a subdirectory in the build -# subdirectory that contains the files needed to run the script without Python - -from cx_Freeze import setup, Executable - -setup( - name = "relimport", - version = "0.1", - description = "Sample cx_Freeze script for relative imports", - executables = [Executable("relimport.py")]) - diff --git a/setup/installer/cx_Freeze/samples/simple/hello.py b/setup/installer/cx_Freeze/samples/simple/hello.py deleted file mode 100644 index 0fb32405bc..0000000000 --- a/setup/installer/cx_Freeze/samples/simple/hello.py +++ /dev/null @@ -1,19 +0,0 @@ -import sys - -print "Hello from cx_Freeze" -print - -print "sys.executable", sys.executable -print "sys.prefix", sys.prefix -print - -print "ARGUMENTS:" -for a in sys.argv: - print a -print - -print "PATH:" -for p in sys.path: - print p -print - diff --git a/setup/installer/cx_Freeze/samples/simple/setup.py b/setup/installer/cx_Freeze/samples/simple/setup.py deleted file mode 100644 index 25de838b10..0000000000 --- a/setup/installer/cx_Freeze/samples/simple/setup.py +++ /dev/null @@ -1,18 +0,0 @@ -# A very simple setup script to create a single executable -# -# hello.py is a very simple "Hello, world" type script which also displays the -# environment in which the script runs -# -# Run the build process by running the command 'python setup.py build' -# -# If everything works well you should find a subdirectory in the build -# subdirectory that contains the files needed to run the script without Python - -from cx_Freeze import setup, Executable - -setup( - name = "hello", - version = "0.1", - description = "Sample cx_Freeze script", - executables = [Executable("hello.py")]) - diff --git a/setup/installer/cx_Freeze/samples/wx/setup.py b/setup/installer/cx_Freeze/samples/wx/setup.py deleted file mode 100644 index 9412996859..0000000000 --- a/setup/installer/cx_Freeze/samples/wx/setup.py +++ /dev/null @@ -1,25 +0,0 @@ -# A simple setup script to create an executable running wxPython. This also -# demonstrates the method for creating a Windows executable that does not have -# an associated console. -# -# wxapp.py is a very simple "Hello, world" type wxPython application -# -# Run the build process by running the command 'python setup.py build' -# -# If everything works well you should find a subdirectory in the build -# subdirectory that contains the files needed to run the application - -import sys - -from cx_Freeze import setup, Executable - -base = None -if sys.platform == "win32": - base = "Win32GUI" - -setup( - name = "hello", - version = "0.1", - description = "Sample cx_Freeze wxPython script", - executables = [Executable("wxapp.py", base = base)]) - diff --git a/setup/installer/cx_Freeze/samples/wx/wxapp.py b/setup/installer/cx_Freeze/samples/wx/wxapp.py deleted file mode 100644 index 7baa90b8d1..0000000000 --- a/setup/installer/cx_Freeze/samples/wx/wxapp.py +++ /dev/null @@ -1,42 +0,0 @@ -import wx - -class Frame(wx.Frame): - - def __init__(self): - wx.Frame.__init__(self, parent = None, title = "Hello from cx_Freeze") - panel = wx.Panel(self) - closeMeButton = wx.Button(panel, -1, "Close Me") - wx.EVT_BUTTON(self, closeMeButton.GetId(), self.OnCloseMe) - wx.EVT_CLOSE(self, self.OnCloseWindow) - pushMeButton = wx.Button(panel, -1, "Push Me") - wx.EVT_BUTTON(self, pushMeButton.GetId(), self.OnPushMe) - sizer = wx.BoxSizer(wx.HORIZONTAL) - sizer.Add(closeMeButton, flag = wx.ALL, border = 20) - sizer.Add(pushMeButton, flag = wx.ALL, border = 20) - panel.SetSizer(sizer) - topSizer = wx.BoxSizer(wx.VERTICAL) - topSizer.Add(panel, flag = wx.ALL | wx.EXPAND) - topSizer.Fit(self) - - def OnCloseMe(self, event): - self.Close(True) - - def OnPushMe(self, event): - 1 / 0 - - def OnCloseWindow(self, event): - self.Destroy() - - -class App(wx.App): - - def OnInit(self): - frame = Frame() - frame.Show(True) - self.SetTopWindow(frame) - return True - - -app = App(1) -app.MainLoop() - diff --git a/setup/installer/cx_Freeze/setup.py b/setup/installer/cx_Freeze/setup.py deleted file mode 100755 index b51453aa7a..0000000000 --- a/setup/installer/cx_Freeze/setup.py +++ /dev/null @@ -1,197 +0,0 @@ -""" -Distutils script for cx_Freeze. -""" - -import distutils.command.bdist_rpm -import distutils.command.build_ext -import distutils.command.build_scripts -import distutils.command.install -import distutils.command.install_data -import distutils.sysconfig -import os -import sys - -from distutils.core import setup -from distutils.extension import Extension - -class bdist_rpm(distutils.command.bdist_rpm.bdist_rpm): - - # rpm automatically byte compiles all Python files in a package but we - # don't want that to happen for initscripts and samples so we tell it to - # ignore those files - def _make_spec_file(self): - specFile = distutils.command.bdist_rpm.bdist_rpm._make_spec_file(self) - specFile.insert(0, "%define _unpackaged_files_terminate_build 0%{nil}") - return specFile - - def run(self): - distutils.command.bdist_rpm.bdist_rpm.run(self) - specFile = os.path.join(self.rpm_base, "SPECS", - "%s.spec" % self.distribution.get_name()) - queryFormat = "%{name}-%{version}-%{release}.%{arch}.rpm" - command = "rpm -q --qf '%s' --specfile %s" % (queryFormat, specFile) - origFileName = os.popen(command).read() - parts = origFileName.split("-") - parts.insert(2, "py%s%s" % sys.version_info[:2]) - newFileName = "-".join(parts) - self.move_file(os.path.join("dist", origFileName), - os.path.join("dist", newFileName)) - - -class build_ext(distutils.command.build_ext.build_ext): - - def build_extension(self, ext): - if ext.name.find("bases") < 0: - distutils.command.build_ext.build_ext.build_extension(self, ext) - return - os.environ["LD_RUN_PATH"] = "${ORIGIN}:${ORIGIN}/../lib" - objects = self.compiler.compile(ext.sources, - output_dir = self.build_temp, - include_dirs = ext.include_dirs, - debug = self.debug, - depends = ext.depends) - fileName = os.path.splitext(self.get_ext_filename(ext.name))[0] - fullName = os.path.join(self.build_lib, fileName) - libraryDirs = ext.library_dirs or [] - libraries = self.get_libraries(ext) - extraArgs = ext.extra_link_args or [] - if sys.platform != "win32": - vars = distutils.sysconfig.get_config_vars() - libraryDirs.append(vars["LIBPL"]) - libraries.append("python%s.%s" % sys.version_info[:2]) - if vars["LINKFORSHARED"]: - extraArgs.extend(vars["LINKFORSHARED"].split()) - if vars["LIBS"]: - extraArgs.extend(vars["LIBS"].split()) - if vars["LIBM"]: - extraArgs.append(vars["LIBM"]) - if vars["BASEMODLIBS"]: - extraArgs.extend(vars["BASEMODLIBS"].split()) - if vars["LOCALMODLIBS"]: - extraArgs.extend(vars["LOCALMODLIBS"].split()) - extraArgs.append("-s") - self.compiler.link_executable(objects, fullName, - libraries = libraries, - library_dirs = libraryDirs, - runtime_library_dirs = ext.runtime_library_dirs, - extra_postargs = extraArgs, - debug = self.debug) - - def get_ext_filename(self, name): - fileName = distutils.command.build_ext.build_ext.get_ext_filename(self, - name) - if name.find("bases") < 0: - return fileName - ext = self.compiler.exe_extension or "" - return os.path.splitext(fileName)[0] + ext - - -class build_scripts(distutils.command.build_scripts.build_scripts): - - def copy_scripts(self): - distutils.command.build_scripts.build_scripts.copy_scripts(self) - if sys.platform == "win32": - for script in self.scripts: - batFileName = os.path.join(self.build_dir, script + ".bat") - fullScriptName = r"%s\Scripts\%s" % \ - (os.path.dirname(sys.executable), script) - command = "%s %s %%1 %%2 %%3 %%4 %%5 %%6 %%7 %%8 %%9" % \ - (sys.executable, fullScriptName) - file(batFileName, "w").write("@echo off\n\n%s" % command) - - -class install(distutils.command.install.install): - - def get_sub_commands(self): - subCommands = distutils.command.install.install.get_sub_commands(self) - subCommands.append("install_packagedata") - return subCommands - - -class install_packagedata(distutils.command.install_data.install_data): - - def run(self): - installCommand = self.get_finalized_command("install") - installDir = getattr(installCommand, "install_lib") - sourceDirs = ["samples", "initscripts"] - while sourceDirs: - sourceDir = sourceDirs.pop(0) - targetDir = os.path.join(installDir, "cx_Freeze", sourceDir) - self.mkpath(targetDir) - for name in os.listdir(sourceDir): - if name == "build" or name.startswith("."): - continue - fullSourceName = os.path.join(sourceDir, name) - if os.path.isdir(fullSourceName): - sourceDirs.append(fullSourceName) - else: - fullTargetName = os.path.join(targetDir, name) - self.copy_file(fullSourceName, fullTargetName) - self.outfiles.append(fullTargetName) - - -commandClasses = dict( - build_ext = build_ext, - build_scripts = build_scripts, - bdist_rpm = bdist_rpm, - install = install, - install_packagedata = install_packagedata) - -if sys.platform == "win32": - libraries = ["imagehlp"] -else: - libraries = [] -utilModule = Extension("cx_Freeze.util", ["source/util.c"], - libraries = libraries) -depends = ["source/bases/Common.c"] -if sys.platform == "win32": - if sys.version_info[:2] >= (2, 6): - extraSources = ["source/bases/manifest.rc"] - else: - extraSources = ["source/bases/dummy.rc"] -else: - extraSources = [] -console = Extension("cx_Freeze.bases.Console", - ["source/bases/Console.c"] + extraSources, depends = depends) -consoleKeepPath = Extension("cx_Freeze.bases.ConsoleKeepPath", - ["source/bases/ConsoleKeepPath.c"] + extraSources, depends = depends) -extensions = [utilModule, console, consoleKeepPath] -if sys.platform == "win32": - gui = Extension("cx_Freeze.bases.Win32GUI", - ["source/bases/Win32GUI.c"] + extraSources, - depends = depends, extra_link_args = ["-mwindows"]) - extensions.append(gui) - -docFiles = "LICENSE.txt README.txt HISTORY.txt doc/cx_Freeze.html" - -classifiers = [ - "Development Status :: 5 - Production/Stable", - "Intended Audience :: Developers", - "License :: OSI Approved :: Python Software Foundation License", - "Natural Language :: English", - "Operating System :: OS Independent", - "Programming Language :: C", - "Programming Language :: Python", - "Topic :: Software Development :: Build Tools", - "Topic :: Software Development :: Libraries :: Python Modules", - "Topic :: System :: Software Distribution", - "Topic :: Utilities" -] - -setup(name = "cx_Freeze", - description = "create standalone executables from Python scripts", - long_description = "create standalone executables from Python scripts", - version = "4.0.1", - cmdclass = commandClasses, - options = dict(bdist_rpm = dict(doc_files = docFiles), - install = dict(optimize = 1)), - ext_modules = extensions, - packages = ['cx_Freeze'], - maintainer="Anthony Tuininga", - maintainer_email="anthony.tuininga@gmail.com", - url = "http://cx-freeze.sourceforge.net", - scripts = ["cxfreeze"], - classifiers = classifiers, - keywords = "freeze", - license = "Python Software Foundation License") - diff --git a/setup/installer/cx_Freeze/source/bases/Common.c b/setup/installer/cx_Freeze/source/bases/Common.c deleted file mode 100644 index ce1e137ebb..0000000000 --- a/setup/installer/cx_Freeze/source/bases/Common.c +++ /dev/null @@ -1,262 +0,0 @@ -//----------------------------------------------------------------------------- -// Common.c -// Routines which are common to running frozen executables. -//----------------------------------------------------------------------------- - -#include -#include -#include - -// global variables (used for simplicity) -static PyObject *g_FileName = NULL; -static PyObject *g_DirName = NULL; -static PyObject *g_ExclusiveZipFileName = NULL; -static PyObject *g_SharedZipFileName = NULL; -static PyObject *g_InitScriptZipFileName = NULL; - -//----------------------------------------------------------------------------- -// GetDirName() -// Return the directory name of the given path. -//----------------------------------------------------------------------------- -static int GetDirName( - const char *path, // path to calculate dir name for - PyObject **dirName) // directory name (OUT) -{ - int i; - - for (i = strlen(path); i > 0 && path[i] != SEP; --i); - *dirName = PyString_FromStringAndSize(path, i); - if (!*dirName) - return FatalError("cannot create string for directory name"); - return 0; -} - - -//----------------------------------------------------------------------------- -// SetExecutableName() -// Set the script to execute and calculate the directory in which the -// executable is found as well as the exclusive (only for this executable) and -// shared zip file names. -//----------------------------------------------------------------------------- -static int SetExecutableName( - const char *fileName) // script to execute -{ - char temp[MAXPATHLEN + 12], *ptr; -#ifndef WIN32 - char linkData[MAXPATHLEN + 1]; - struct stat statData; - size_t linkSize, i; - PyObject *dirName; -#endif - - // store file name - g_FileName = PyString_FromString(fileName); - if (!g_FileName) - return FatalError("cannot create string for file name"); - -#ifndef WIN32 - for (i = 0; i < 25; i++) { - if (lstat(fileName, &statData) < 0) { - PyErr_SetFromErrnoWithFilename(PyExc_OSError, (char*) fileName); - return FatalError("unable to stat file"); - } - if (!S_ISLNK(statData.st_mode)) - break; - linkSize = readlink(fileName, linkData, sizeof(linkData)); - if (linkSize < 0) { - PyErr_SetFromErrnoWithFilename(PyExc_OSError, (char*) fileName); - return FatalError("unable to stat file"); - } - if (linkData[0] == '/') { - Py_DECREF(g_FileName); - g_FileName = PyString_FromStringAndSize(linkData, linkSize); - } else { - if (GetDirName(PyString_AS_STRING(g_FileName), &dirName) < 0) - return -1; - if (PyString_GET_SIZE(dirName) + linkSize + 1 > MAXPATHLEN) { - Py_DECREF(dirName); - return FatalError("cannot dereference link, path too large"); - } - strcpy(temp, PyString_AS_STRING(dirName)); - strcat(temp, "/"); - strcat(temp, linkData); - Py_DECREF(g_FileName); - g_FileName = PyString_FromString(temp); - } - if (!g_FileName) - return FatalError("cannot create string for linked file name"); - fileName = PyString_AS_STRING(g_FileName); - } -#endif - - // calculate and store directory name - if (GetDirName(fileName, &g_DirName) < 0) - return -1; - - // calculate and store exclusive zip file name - strcpy(temp, fileName); - ptr = temp + strlen(temp) - 1; - while (ptr > temp && *ptr != SEP && *ptr != '.') - ptr--; - if (*ptr == '.') - *ptr = '\0'; - strcat(temp, ".zip"); - g_ExclusiveZipFileName = PyString_FromString(temp); - if (!g_ExclusiveZipFileName) - return FatalError("cannot create string for exclusive zip file name"); - - // calculate and store shared zip file name - strcpy(temp, PyString_AS_STRING(g_DirName)); - ptr = temp + strlen(temp); - *ptr++ = SEP; - strcpy(ptr, "library.zip"); - g_SharedZipFileName = PyString_FromString(temp); - if (!g_SharedZipFileName) - return FatalError("cannot create string for shared zip file name"); - - return 0; -} - - -//----------------------------------------------------------------------------- -// SetPathToSearch() -// Set the path to search. This includes the file (for those situations where -// a zip file is attached to the executable itself), the directory where the -// executable is found (to search for extensions), the exclusive zip file -// name and the shared zip file name. -//----------------------------------------------------------------------------- -static int SetPathToSearch(void) -{ - PyObject *pathList; - - pathList = PySys_GetObject("path"); - if (!pathList) - return FatalError("cannot acquire sys.path"); - if (PyList_Insert(pathList, 0, g_FileName) < 0) - return FatalError("cannot insert file name into sys.path"); - if (PyList_Insert(pathList, 1, g_DirName) < 0) - return FatalError("cannot insert directory name into sys.path"); - if (PyList_Insert(pathList, 2, g_ExclusiveZipFileName) < 0) - return FatalError("cannot insert exclusive zip name into sys.path"); - if (PyList_Insert(pathList, 3, g_SharedZipFileName) < 0) - return FatalError("cannot insert shared zip name into sys.path"); - return 0; -} - - -//----------------------------------------------------------------------------- -// GetImporterHelper() -// Helper which is used to locate the importer for the initscript. -//----------------------------------------------------------------------------- -static PyObject *GetImporterHelper( - PyObject *module, // zipimport module - PyObject *fileName) // name of file to search -{ - PyObject *importer; - - importer = PyObject_CallMethod(module, "zipimporter", "O", fileName); - if (importer) - g_InitScriptZipFileName = fileName; - else - PyErr_Clear(); - return importer; -} - - -//----------------------------------------------------------------------------- -// GetImporter() -// Return the importer which will be used for importing the initialization -// script. The executable itself is searched first, followed by the exclusive -// zip file and finally by the shared zip file. -//----------------------------------------------------------------------------- -static int GetImporter( - PyObject **importer) // importer (OUT) -{ - PyObject *module; - - module = PyImport_ImportModule("zipimport"); - if (!module) - return FatalError("cannot import zipimport module"); - *importer = GetImporterHelper(module, g_FileName); - if (!*importer) { - *importer = GetImporterHelper(module, g_ExclusiveZipFileName); - if (!*importer) - *importer = GetImporterHelper(module, g_SharedZipFileName); - } - Py_DECREF(module); - if (!*importer) - return FatalError("cannot get zipimporter instance"); - return 0; -} - - -//----------------------------------------------------------------------------- -// PopulateInitScriptDict() -// Return the dictionary used by the initialization script. -//----------------------------------------------------------------------------- -static int PopulateInitScriptDict( - PyObject *dict) // dictionary to populate -{ - if (!dict) - return FatalError("unable to create temporary dictionary"); - if (PyDict_SetItemString(dict, "__builtins__", PyEval_GetBuiltins()) < 0) - return FatalError("unable to set __builtins__"); - if (PyDict_SetItemString(dict, "FILE_NAME", g_FileName) < 0) - return FatalError("unable to set FILE_NAME"); - if (PyDict_SetItemString(dict, "DIR_NAME", g_DirName) < 0) - return FatalError("unable to set DIR_NAME"); - if (PyDict_SetItemString(dict, "EXCLUSIVE_ZIP_FILE_NAME", - g_ExclusiveZipFileName) < 0) - return FatalError("unable to set EXCLUSIVE_ZIP_FILE_NAME"); - if (PyDict_SetItemString(dict, "SHARED_ZIP_FILE_NAME", - g_SharedZipFileName) < 0) - return FatalError("unable to set SHARED_ZIP_FILE_NAME"); - if (PyDict_SetItemString(dict, "INITSCRIPT_ZIP_FILE_NAME", - g_InitScriptZipFileName) < 0) - return FatalError("unable to set INITSCRIPT_ZIP_FILE_NAME"); - return 0; -} - - - - -//----------------------------------------------------------------------------- -// ExecuteScript() -// Execute the script found within the file. -//----------------------------------------------------------------------------- -static int ExecuteScript( - const char *fileName) // name of file containing Python code -{ - PyObject *importer, *dict, *code, *temp; - - if (SetExecutableName(fileName) < 0) - return -1; - if (SetPathToSearch() < 0) - return -1; - importer = NULL; - if (GetImporter(&importer) < 0) - return -1; - - // create and populate dictionary for initscript module - dict = PyDict_New(); - if (PopulateInitScriptDict(dict) < 0) { - Py_XDECREF(dict); - Py_DECREF(importer); - return -1; - } - - // locate and execute script - code = PyObject_CallMethod(importer, "get_code", "s", "cx_Freeze__init__"); - Py_DECREF(importer); - if (!code) - return FatalError("unable to locate initialization module"); - temp = PyEval_EvalCode( (PyCodeObject*) code, dict, dict); - Py_DECREF(code); - Py_DECREF(dict); - if (!temp) - return FatalScriptError(); - Py_DECREF(temp); - - return 0; -} - diff --git a/setup/installer/cx_Freeze/source/bases/Console.c b/setup/installer/cx_Freeze/source/bases/Console.c deleted file mode 100644 index d6a8a515a0..0000000000 --- a/setup/installer/cx_Freeze/source/bases/Console.c +++ /dev/null @@ -1,72 +0,0 @@ -//----------------------------------------------------------------------------- -// Console.c -// Main routine for frozen programs which run in a console. -//----------------------------------------------------------------------------- - -#include -#ifdef __WIN32__ -#include -#endif - -//----------------------------------------------------------------------------- -// FatalError() -// Prints a fatal error. -//----------------------------------------------------------------------------- -static int FatalError( - const char *message) // message to print -{ - PyErr_Print(); - Py_FatalError(message); - return -1; -} - - -//----------------------------------------------------------------------------- -// FatalScriptError() -// Prints a fatal error in the initialization script. -//----------------------------------------------------------------------------- -static int FatalScriptError(void) -{ - PyErr_Print(); - return -1; -} - - -#include "Common.c" - - -//----------------------------------------------------------------------------- -// main() -// Main routine for frozen programs. -//----------------------------------------------------------------------------- -int main(int argc, char **argv) -{ - const char *fileName; - char *encoding; - - // initialize Python - Py_NoSiteFlag = 1; - Py_FrozenFlag = 1; - Py_IgnoreEnvironmentFlag = 1; - - encoding = getenv("PYTHONIOENCODING"); - if (encoding != NULL) { - Py_FileSystemDefaultEncoding = strndup(encoding, 100); - } - - Py_SetPythonHome(""); - Py_SetProgramName(argv[0]); - fileName = Py_GetProgramFullPath(); - - Py_Initialize(); - PySys_SetArgv(argc, argv); - - - // do the work - if (ExecuteScript(fileName) < 0) - return 1; - - Py_Finalize(); - return 0; -} - diff --git a/setup/installer/cx_Freeze/source/bases/ConsoleKeepPath.c b/setup/installer/cx_Freeze/source/bases/ConsoleKeepPath.c deleted file mode 100644 index 3ad00f8488..0000000000 --- a/setup/installer/cx_Freeze/source/bases/ConsoleKeepPath.c +++ /dev/null @@ -1,60 +0,0 @@ -//----------------------------------------------------------------------------- -// ConsoleKeepPath.c -// Main routine for frozen programs which need a Python installation to do -// their work. -//----------------------------------------------------------------------------- - -#include -#ifdef __WIN32__ -#include -#endif - -//----------------------------------------------------------------------------- -// FatalError() -// Prints a fatal error. -//----------------------------------------------------------------------------- -static int FatalError( - const char *message) // message to print -{ - PyErr_Print(); - Py_FatalError(message); - return -1; -} - - -//----------------------------------------------------------------------------- -// FatalScriptError() -// Prints a fatal error in the initialization script. -//----------------------------------------------------------------------------- -static int FatalScriptError(void) -{ - PyErr_Print(); - return -1; -} - - -#include "Common.c" - - -//----------------------------------------------------------------------------- -// main() -// Main routine for frozen programs. -//----------------------------------------------------------------------------- -int main(int argc, char **argv) -{ - const char *fileName; - - // initialize Python - Py_SetProgramName(argv[0]); - fileName = Py_GetProgramFullPath(); - Py_Initialize(); - PySys_SetArgv(argc, argv); - - // do the work - if (ExecuteScript(fileName) < 0) - return 1; - - Py_Finalize(); - return 0; -} - diff --git a/setup/installer/cx_Freeze/source/bases/Win32GUI.c b/setup/installer/cx_Freeze/source/bases/Win32GUI.c deleted file mode 100644 index f5bbe74dba..0000000000 --- a/setup/installer/cx_Freeze/source/bases/Win32GUI.c +++ /dev/null @@ -1,242 +0,0 @@ -//----------------------------------------------------------------------------- -// Win32GUI.c -// Main routine for frozen programs written for the Win32 GUI subsystem. -//----------------------------------------------------------------------------- - -#include -#include - -//----------------------------------------------------------------------------- -// FatalError() -// Handle a fatal error. -//----------------------------------------------------------------------------- -static int FatalError( - char *a_Message) // message to display -{ - MessageBox(NULL, a_Message, "cx_Freeze Fatal Error", MB_ICONERROR); - Py_Finalize(); - return -1; -} - - -//----------------------------------------------------------------------------- -// StringifyObject() -// Stringify a Python object. -//----------------------------------------------------------------------------- -static char *StringifyObject( - PyObject *object, // object to stringify - PyObject **stringRep) // string representation -{ - if (object) { - *stringRep = PyObject_Str(object); - if (*stringRep) - return PyString_AS_STRING(*stringRep); - return "Unable to stringify"; - } - - // object is NULL - *stringRep = NULL; - return "None"; -} - - -//----------------------------------------------------------------------------- -// FatalPythonErrorNoTraceback() -// Handle a fatal Python error without traceback. -//----------------------------------------------------------------------------- -static int FatalPythonErrorNoTraceback( - PyObject *origType, // exception type - PyObject *origValue, // exception value - char *message) // message to display -{ - PyObject *typeStrRep, *valueStrRep, *origTypeStrRep, *origValueStrRep; - char *totalMessage, *typeStr, *valueStr, *origTypeStr, *origValueStr; - PyObject *type, *value, *traceback; - int totalMessageLength; - char *messageFormat; - - // fetch error and string representations of the error - PyErr_Fetch(&type, &value, &traceback); - origTypeStr = StringifyObject(origType, &origTypeStrRep); - origValueStr = StringifyObject(origValue, &origValueStrRep); - typeStr = StringifyObject(type, &typeStrRep); - valueStr = StringifyObject(value, &valueStrRep); - - // fill out the message to be displayed - messageFormat = "Type: %s\nValue: %s\nOther Type: %s\nOtherValue: %s\n%s"; - totalMessageLength = strlen(origTypeStr) + strlen(origValueStr) + - strlen(typeStr) + strlen(valueStr) + strlen(message) + - strlen(messageFormat) + 1; - totalMessage = malloc(totalMessageLength); - if (!totalMessage) - return FatalError("Out of memory!"); - sprintf(totalMessage, messageFormat, typeStr, valueStr, origTypeStr, - origValueStr, message); - - // display the message - MessageBox(NULL, totalMessage, - "cx_Freeze: Python error in main script (traceback unavailable)", - MB_ICONERROR); - free(totalMessage); - return -1; -} - - -//----------------------------------------------------------------------------- -// ArgumentValue() -// Return a suitable argument value by replacing NULL with Py_None. -//----------------------------------------------------------------------------- -static PyObject *ArgumentValue( - PyObject *object) // argument to massage -{ - if (object) { - Py_INCREF(object); - return object; - } - Py_INCREF(Py_None); - return Py_None; -} - - -//----------------------------------------------------------------------------- -// HandleSystemExitException() -// Handles a system exit exception differently. If an integer value is passed -// through then that becomes the exit value; otherwise the string value of the -// value passed through is displayed in a message box. -//----------------------------------------------------------------------------- -static void HandleSystemExitException() -{ - PyObject *type, *value, *traceback, *valueStr; - int exitCode = 0; - char *message; - - PyErr_Fetch(&type, &value, &traceback); - if (PyInstance_Check(value)) { - PyObject *code = PyObject_GetAttrString(value, "code"); - if (code) { - Py_DECREF(value); - value = code; - if (value == Py_None) - Py_Exit(0); - } - } - if (PyInt_Check(value)) - exitCode = PyInt_AsLong(value); - else { - message = StringifyObject(value, &valueStr); - MessageBox(NULL, message, "cx_Freeze: Application Terminated", - MB_ICONERROR); - Py_XDECREF(valueStr); - exitCode = 1; - } - Py_Exit(exitCode); -} - - -//----------------------------------------------------------------------------- -// FatalScriptError() -// Handle a fatal Python error with traceback. -//----------------------------------------------------------------------------- -static int FatalScriptError() -{ - PyObject *type, *value, *traceback, *argsTuple, *module, *method, *result; - int tracebackLength, i; - char *tracebackStr; - - // if a system exception, handle it specially - if (PyErr_ExceptionMatches(PyExc_SystemExit)) - HandleSystemExitException(); - - // get the exception details - PyErr_Fetch(&type, &value, &traceback); - - // import the traceback module - module = PyImport_ImportModule("traceback"); - if (!module) - return FatalPythonErrorNoTraceback(type, value, - "Cannot import traceback module."); - - // get the format_exception method - method = PyObject_GetAttrString(module, "format_exception"); - Py_DECREF(module); - if (!method) - return FatalPythonErrorNoTraceback(type, value, - "Cannot get format_exception method."); - - // create a tuple for the arguments - argsTuple = PyTuple_New(3); - if (!argsTuple) { - Py_DECREF(method); - return FatalPythonErrorNoTraceback(type, value, - "Cannot create arguments tuple for traceback."); - } - PyTuple_SET_ITEM(argsTuple, 0, ArgumentValue(type)); - PyTuple_SET_ITEM(argsTuple, 1, ArgumentValue(value)); - PyTuple_SET_ITEM(argsTuple, 2, ArgumentValue(traceback)); - - // call the format_exception method - result = PyObject_CallObject(method, argsTuple); - Py_DECREF(method); - Py_DECREF(argsTuple); - if (!result) - return FatalPythonErrorNoTraceback(type, value, - "Failed calling format_exception method."); - - // determine length of string representation of formatted traceback - tracebackLength = 1; - for (i = 0; i < PyList_GET_SIZE(result); i++) - tracebackLength += PyString_GET_SIZE(PyList_GET_ITEM(result, i)); - - // create a string representation of the formatted traceback - tracebackStr = malloc(tracebackLength); - if (!tracebackStr) { - Py_DECREF(result); - return FatalError("Out of memory!"); - } - tracebackStr[0] = '\0'; - for (i = 0; i < PyList_GET_SIZE(result); i++) - strcat(tracebackStr, PyString_AS_STRING(PyList_GET_ITEM(result, i))); - Py_DECREF(result); - - // bring up the error - MessageBox(NULL, tracebackStr, "cx_Freeze: Python error in main script", - MB_ICONERROR); - Py_Finalize(); - return 1; -} - - -#include "Common.c" - - -//----------------------------------------------------------------------------- -// WinMain() -// Main routine for the executable in Windows. -//----------------------------------------------------------------------------- -int WINAPI WinMain( - HINSTANCE instance, // handle to application - HINSTANCE prevInstance, // previous handle to application - LPSTR commandLine, // command line - int showFlag) // show flag -{ - const char *fileName; - - // initialize Python - Py_NoSiteFlag = 1; - Py_FrozenFlag = 1; - Py_IgnoreEnvironmentFlag = 1; - Py_SetPythonHome(""); - Py_SetProgramName(__argv[0]); - fileName = Py_GetProgramFullPath(); - Py_Initialize(); - PySys_SetArgv(__argc, __argv); - - // do the work - if (ExecuteScript(fileName) < 0) - return 1; - - // terminate Python - Py_Finalize(); - return 0; -} - diff --git a/setup/installer/cx_Freeze/source/bases/dummy.rc b/setup/installer/cx_Freeze/source/bases/dummy.rc deleted file mode 100644 index 5c1fa1a194..0000000000 --- a/setup/installer/cx_Freeze/source/bases/dummy.rc +++ /dev/null @@ -1,5 +0,0 @@ -STRINGTABLE -{ - 1, "Just to ensure that buggy EndUpdateResource doesn't fall over." -} - diff --git a/setup/installer/cx_Freeze/source/bases/manifest.rc b/setup/installer/cx_Freeze/source/bases/manifest.rc deleted file mode 100644 index 2b7ee27ab5..0000000000 --- a/setup/installer/cx_Freeze/source/bases/manifest.rc +++ /dev/null @@ -1,3 +0,0 @@ -#include "dummy.rc" - -1 24 source/bases/manifest.txt diff --git a/setup/installer/cx_Freeze/source/util.c b/setup/installer/cx_Freeze/source/util.c deleted file mode 100644 index 1c8eb0c0ca..0000000000 --- a/setup/installer/cx_Freeze/source/util.c +++ /dev/null @@ -1,418 +0,0 @@ -//----------------------------------------------------------------------------- -// util.c -// Shared library for use by cx_Freeze. -//----------------------------------------------------------------------------- - -#include - -#ifdef WIN32 -#include -#include - -#pragma pack(2) - -typedef struct { - BYTE bWidth; // Width, in pixels, of the image - BYTE bHeight; // Height, in pixels, of the image - BYTE bColorCount; // Number of colors in image - BYTE bReserved; // Reserved ( must be 0) - WORD wPlanes; // Color Planes - WORD wBitCount; // Bits per pixel - DWORD dwBytesInRes; // How many bytes in this resource? - DWORD dwImageOffset; // Where in the file is this image? -} ICONDIRENTRY; - -typedef struct { - WORD idReserved; // Reserved (must be 0) - WORD idType; // Resource Type (1 for icons) - WORD idCount; // How many images? - ICONDIRENTRY idEntries[0]; // An entry for each image -} ICONDIR; - -typedef struct { - BYTE bWidth; // Width, in pixels, of the image - BYTE bHeight; // Height, in pixels, of the image - BYTE bColorCount; // Number of colors in image - BYTE bReserved; // Reserved ( must be 0) - WORD wPlanes; // Color Planes - WORD wBitCount; // Bits per pixel - DWORD dwBytesInRes; // How many bytes in this resource? - WORD nID; // resource ID -} GRPICONDIRENTRY; - -typedef struct { - WORD idReserved; // Reserved (must be 0) - WORD idType; // Resource Type (1 for icons) - WORD idCount; // How many images? - GRPICONDIRENTRY idEntries[0]; // An entry for each image -} GRPICONDIR; -#endif - -//----------------------------------------------------------------------------- -// Globals -//----------------------------------------------------------------------------- -#ifdef WIN32 -static PyObject *g_BindErrorException = NULL; -static PyObject *g_ImageNames = NULL; -#endif - - -#ifdef WIN32 -//----------------------------------------------------------------------------- -// BindStatusRoutine() -// Called by BindImageEx() at various points. This is used to determine the -// dependency tree which is later examined by cx_Freeze. -//----------------------------------------------------------------------------- -static BOOL __stdcall BindStatusRoutine( - IMAGEHLP_STATUS_REASON reason, // reason called - PSTR imageName, // name of image being examined - PSTR dllName, // name of DLL - ULONG virtualAddress, // computed virtual address - ULONG parameter) // parameter (value depends on reason) -{ - char fileName[MAX_PATH + 1]; - - switch (reason) { - case BindImportModule: - if (!SearchPath(NULL, dllName, NULL, sizeof(fileName), fileName, - NULL)) - return FALSE; - Py_INCREF(Py_None); - if (PyDict_SetItemString(g_ImageNames, fileName, Py_None) < 0) - return FALSE; - break; - default: - break; - } - return TRUE; -} - - -//----------------------------------------------------------------------------- -// GetFileData() -// Return the data for the given file. -//----------------------------------------------------------------------------- -static int GetFileData( - const char *fileName, // name of file to read - char **data) // pointer to data (OUT) -{ - DWORD numberOfBytesRead, dataSize; - HANDLE file; - - file = CreateFile(fileName, GENERIC_READ, FILE_SHARE_READ, NULL, - OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); - if (file == INVALID_HANDLE_VALUE) - return -1; - dataSize = GetFileSize(file, NULL); - if (dataSize == INVALID_FILE_SIZE) { - CloseHandle(file); - return -1; - } - *data = PyMem_Malloc(dataSize); - if (!*data) { - CloseHandle(file); - return -1; - } - if (!ReadFile(file, *data, dataSize, &numberOfBytesRead, NULL)) { - CloseHandle(file); - return -1; - } - CloseHandle(file); - return 0; -} - - -//----------------------------------------------------------------------------- -// CreateGroupIconResource() -// Return the group icon resource given the icon file data. -//----------------------------------------------------------------------------- -static GRPICONDIR *CreateGroupIconResource( - ICONDIR *iconDir, // icon information - DWORD *resourceSize) // size of resource (OUT) -{ - GRPICONDIR *groupIconDir; - int i; - - *resourceSize = sizeof(GRPICONDIR) + - sizeof(GRPICONDIRENTRY) * iconDir->idCount; - groupIconDir = PyMem_Malloc(*resourceSize); - if (!groupIconDir) - return NULL; - groupIconDir->idReserved = iconDir->idReserved; - groupIconDir->idType = iconDir->idType; - groupIconDir->idCount = iconDir->idCount; - for (i = 0; i < iconDir->idCount; i++) { - groupIconDir->idEntries[i].bWidth = iconDir->idEntries[i].bWidth; - groupIconDir->idEntries[i].bHeight = iconDir->idEntries[i].bHeight; - groupIconDir->idEntries[i].bColorCount = - iconDir->idEntries[i].bColorCount; - groupIconDir->idEntries[i].bReserved = iconDir->idEntries[i].bReserved; - groupIconDir->idEntries[i].wPlanes = iconDir->idEntries[i].wPlanes; - groupIconDir->idEntries[i].wBitCount = iconDir->idEntries[i].wBitCount; - groupIconDir->idEntries[i].dwBytesInRes = - iconDir->idEntries[i].dwBytesInRes; - groupIconDir->idEntries[i].nID = i + 1; - } - - return groupIconDir; -} - - -//----------------------------------------------------------------------------- -// ExtAddIcon() -// Add the icon as a resource to the specified file. -//----------------------------------------------------------------------------- -static PyObject *ExtAddIcon( - PyObject *self, // passthrough argument - PyObject *args) // arguments -{ - char *executableName, *iconName, *data, *iconData; - GRPICONDIR *groupIconDir; - DWORD resourceSize; - ICONDIR *iconDir; - BOOL succeeded; - HANDLE handle; - int i; - - if (!PyArg_ParseTuple(args, "ss", &executableName, &iconName)) - return NULL; - - // begin updating the executable - handle = BeginUpdateResource(executableName, FALSE); - if (!handle) { - PyErr_SetExcFromWindowsErrWithFilename(PyExc_WindowsError, - GetLastError(), executableName); - return NULL; - } - - // first attempt to get the data from the icon file - data = NULL; - succeeded = TRUE; - groupIconDir = NULL; - if (GetFileData(iconName, &data) < 0) - succeeded = FALSE; - iconDir = (ICONDIR*) data; - - // next, attempt to add a group icon resource - if (succeeded) { - groupIconDir = CreateGroupIconResource(iconDir, &resourceSize); - if (groupIconDir) - succeeded = UpdateResource(handle, RT_GROUP_ICON, - MAKEINTRESOURCE(1), - MAKELANGID(LANG_NEUTRAL, SUBLANG_NEUTRAL), - groupIconDir, resourceSize); - else succeeded = FALSE; - } - - // next, add each icon as a resource - if (succeeded) { - for (i = 0; i < iconDir->idCount; i++) { - iconData = &data[iconDir->idEntries[i].dwImageOffset]; - resourceSize = iconDir->idEntries[i].dwBytesInRes; - succeeded = UpdateResource(handle, RT_ICON, MAKEINTRESOURCE(i + 1), - MAKELANGID(LANG_NEUTRAL, SUBLANG_NEUTRAL), iconData, - resourceSize); - if (!succeeded) - break; - } - } - - // finish writing the resource (or discarding the changes upon an error) - if (!EndUpdateResource(handle, !succeeded)) { - if (succeeded) { - succeeded = FALSE; - PyErr_SetExcFromWindowsErrWithFilename(PyExc_WindowsError, - GetLastError(), executableName); - } - } - - // clean up - if (groupIconDir) - PyMem_Free(groupIconDir); - if (data) - PyMem_Free(data); - if (!succeeded) - return NULL; - - Py_INCREF(Py_None); - return Py_None; -} - - -//----------------------------------------------------------------------------- -// ExtBeginUpdateResource() -// Wrapper for BeginUpdateResource(). -//----------------------------------------------------------------------------- -static PyObject *ExtBeginUpdateResource( - PyObject *self, // passthrough argument - PyObject *args) // arguments -{ - BOOL deleteExistingResources; - char *fileName; - HANDLE handle; - - deleteExistingResources = TRUE; - if (!PyArg_ParseTuple(args, "s|i", &fileName, &deleteExistingResources)) - return NULL; - handle = BeginUpdateResource(fileName, deleteExistingResources); - if (!handle) { - PyErr_SetExcFromWindowsErrWithFilename(PyExc_WindowsError, - GetLastError(), fileName); - return NULL; - } - return PyInt_FromLong((long) handle); -} - - -//----------------------------------------------------------------------------- -// ExtUpdateResource() -// Wrapper for UpdateResource(). -//----------------------------------------------------------------------------- -static PyObject *ExtUpdateResource( - PyObject *self, // passthrough argument - PyObject *args) // arguments -{ - int resourceType, resourceId, resourceDataSize; - char *resourceData; - HANDLE handle; - - if (!PyArg_ParseTuple(args, "iiis#", &handle, &resourceType, &resourceId, - &resourceData, &resourceDataSize)) - return NULL; - if (!UpdateResource(handle, MAKEINTRESOURCE(resourceType), - MAKEINTRESOURCE(resourceId), - MAKELANGID(LANG_NEUTRAL, SUBLANG_NEUTRAL), resourceData, - resourceDataSize)) { - PyErr_SetExcFromWindowsErr(PyExc_WindowsError, GetLastError()); - return NULL; - } - - Py_INCREF(Py_None); - return Py_None; -} - - -//----------------------------------------------------------------------------- -// ExtEndUpdateResource() -// Wrapper for EndUpdateResource(). -//----------------------------------------------------------------------------- -static PyObject *ExtEndUpdateResource( - PyObject *self, // passthrough argument - PyObject *args) // arguments -{ - BOOL discardChanges; - HANDLE handle; - - discardChanges = FALSE; - if (!PyArg_ParseTuple(args, "i|i", &handle, &discardChanges)) - return NULL; - if (!EndUpdateResource(handle, discardChanges)) { - PyErr_SetExcFromWindowsErr(PyExc_WindowsError, GetLastError()); - return NULL; - } - - Py_INCREF(Py_None); - return Py_None; -} - - -//----------------------------------------------------------------------------- -// ExtGetDependentFiles() -// Return a list of files that this file depends on. -//----------------------------------------------------------------------------- -static PyObject *ExtGetDependentFiles( - PyObject *self, // passthrough argument - PyObject *args) // arguments -{ - PyObject *results; - char *imageName; - - if (!PyArg_ParseTuple(args, "s", &imageName)) - return NULL; - g_ImageNames = PyDict_New(); - if (!g_ImageNames) - return NULL; - if (!BindImageEx(BIND_NO_BOUND_IMPORTS | BIND_NO_UPDATE | BIND_ALL_IMAGES, - imageName, NULL, NULL, BindStatusRoutine)) { - Py_DECREF(g_ImageNames); - PyErr_SetExcFromWindowsErrWithFilename(g_BindErrorException, - GetLastError(), imageName); - return NULL; - } - results = PyDict_Keys(g_ImageNames); - Py_DECREF(g_ImageNames); - return results; -} - - -//----------------------------------------------------------------------------- -// ExtGetSystemDir() -// Return the Windows directory (C:\Windows for example). -//----------------------------------------------------------------------------- -static PyObject *ExtGetSystemDir( - PyObject *self, // passthrough argument - PyObject *args) // arguments (ignored) -{ - char dir[MAX_PATH + 1]; - - if (GetSystemDirectory(dir, sizeof(dir))) - return PyString_FromString(dir); - PyErr_SetExcFromWindowsErr(PyExc_RuntimeError, GetLastError()); - return NULL; -} -#endif - - -//----------------------------------------------------------------------------- -// ExtSetOptimizeFlag() -// Set the optimize flag as needed. -//----------------------------------------------------------------------------- -static PyObject *ExtSetOptimizeFlag( - PyObject *self, // passthrough argument - PyObject *args) // arguments -{ - if (!PyArg_ParseTuple(args, "i", &Py_OptimizeFlag)) - return NULL; - Py_INCREF(Py_None); - return Py_None; -} - - -//----------------------------------------------------------------------------- -// Methods -//----------------------------------------------------------------------------- -static PyMethodDef g_ModuleMethods[] = { - { "SetOptimizeFlag", ExtSetOptimizeFlag, METH_VARARGS }, -#ifdef WIN32 - { "BeginUpdateResource", ExtBeginUpdateResource, METH_VARARGS }, - { "UpdateResource", ExtUpdateResource, METH_VARARGS }, - { "EndUpdateResource", ExtEndUpdateResource, METH_VARARGS }, - { "AddIcon", ExtAddIcon, METH_VARARGS }, - { "GetDependentFiles", ExtGetDependentFiles, METH_VARARGS }, - { "GetSystemDir", ExtGetSystemDir, METH_NOARGS }, -#endif - { NULL } -}; - - -//----------------------------------------------------------------------------- -// initutil() -// Initialization routine for the shared libary. -//----------------------------------------------------------------------------- -void initutil(void) -{ - PyObject *module; - - module = Py_InitModule("cx_Freeze.util", g_ModuleMethods); - if (!module) - return; -#ifdef WIN32 - g_BindErrorException = PyErr_NewException("cx_Freeze.util.BindError", - NULL, NULL); - if (!g_BindErrorException) - return; - if (PyModule_AddObject(module, "BindError", g_BindErrorException) < 0) - return; -#endif -} - From 8f42c11aa34f9ecd721de8625757edaa10cfb454 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 14 Aug 2010 08:17:43 -0600 Subject: [PATCH 02/73] GoComics by Starson17. Fixes #6500 (New Recipe: GoComics) --- resources/recipes/go_comics.recipe | 348 +++++++++++++++++++++++++++++ 1 file changed, 348 insertions(+) create mode 100644 resources/recipes/go_comics.recipe diff --git a/resources/recipes/go_comics.recipe b/resources/recipes/go_comics.recipe new file mode 100644 index 0000000000..b98b628942 --- /dev/null +++ b/resources/recipes/go_comics.recipe @@ -0,0 +1,348 @@ +#!/usr/bin/env python + +__license__ = 'GPL v3' +__copyright__ = 'Copyright 2010 Starson17' +''' +www.gocomics.com +''' +from calibre.web.feeds.news import BasicNewsRecipe +import mechanize + +class GoComics(BasicNewsRecipe): + title = 'GoComics' + __author__ = 'Starson17' + __version__ = '1.02' + __date__ = '14 August 2010' + description = u'200+ Comics - Customize for more days/comics: Defaults to 7 days, 25 comics - 20 general, 5 editorial.' + category = 'news, comics' + language = 'en' + use_embedded_content= False + no_stylesheets = True + remove_javascript = True + cover_url = 'http://paulbuckley14059.files.wordpress.com/2008/06/calvin-and-hobbes.jpg' + + ####### USER PREFERENCES - COMICS, IMAGE SIZE AND NUMBER OF COMICS TO RETRIEVE ######## + # num_comics_to_get - I've tried up to 99 on Calvin&Hobbes + num_comics_to_get = 7 + # comic_size 300 is small, 600 is medium, 900 is large, 1500 is extra-large + comic_size = 900 + # CHOOSE COMIC STRIPS BELOW - REMOVE COMMENT '# ' FROM IN FRONT OF DESIRED STRIPS + # Please do not overload their servers by selecting all comics and 1000 strips from each! + + conversion_options = {'linearize_tables' : True + , 'comment' : description + , 'tags' : category + , 'language' : language + } + + keep_only_tags = [dict(name='div', attrs={'class':['feature','banner']}), + ] + + remove_tags = [dict(name='a', attrs={'class':['beginning','prev','cal','next','newest']}), + dict(name='div', attrs={'class':['tag-wrapper']}), + dict(name='ul', attrs={'class':['share-nav','feature-nav']}), + ] + + def get_browser(self): + br = BasicNewsRecipe.get_browser(self) + cookies = mechanize.CookieJar() + br = mechanize.build_opener(mechanize.HTTPCookieProcessor(cookies)) + br.addheaders = [('Referer','http://www.gocomics.com/')] + return br + + def parse_index(self): + feeds = [] + for title, url in [ + ######## COMICS - GENERAL ######## + (u"2 Cows and a Chicken", u"http://www.gocomics.com/2cowsandachicken"), + # (u"9 to 5", u"http://www.gocomics.com/9to5"), + # (u"The Academia Waltz", u"http://www.gocomics.com/academiawaltz"), + # (u"Adam@Home", u"http://www.gocomics.com/adamathome"), + # (u"Agnes", u"http://www.gocomics.com/agnes"), + # (u"Andy Capp", u"http://www.gocomics.com/andycapp"), + # (u"Animal Crackers", u"http://www.gocomics.com/animalcrackers"), + # (u"Annie", u"http://www.gocomics.com/annie"), + (u"The Argyle Sweater", u"http://www.gocomics.com/theargylesweater"), + # (u"Ask Shagg", u"http://www.gocomics.com/askshagg"), + (u"B.C.", u"http://www.gocomics.com/bc"), + # (u"Back in the Day", u"http://www.gocomics.com/backintheday"), + # (u"Bad Reporter", u"http://www.gocomics.com/badreporter"), + # (u"Baldo", u"http://www.gocomics.com/baldo"), + # (u"Ballard Street", u"http://www.gocomics.com/ballardstreet"), + # (u"Barkeater Lake", u"http://www.gocomics.com/barkeaterlake"), + # (u"The Barn", u"http://www.gocomics.com/thebarn"), + # (u"Basic Instructions", u"http://www.gocomics.com/basicinstructions"), + # (u"Bewley", u"http://www.gocomics.com/bewley"), + # (u"Big Top", u"http://www.gocomics.com/bigtop"), + # (u"Biographic", u"http://www.gocomics.com/biographic"), + (u"Birdbrains", u"http://www.gocomics.com/birdbrains"), + # (u"Bleeker: The Rechargeable Dog", u"http://www.gocomics.com/bleeker"), + # (u"Bliss", u"http://www.gocomics.com/bliss"), + (u"Bloom County", u"http://www.gocomics.com/bloomcounty"), + # (u"Bo Nanas", u"http://www.gocomics.com/bonanas"), + # (u"Bob the Squirrel", u"http://www.gocomics.com/bobthesquirrel"), + # (u"The Boiling Point", u"http://www.gocomics.com/theboilingpoint"), + # (u"Boomerangs", u"http://www.gocomics.com/boomerangs"), + # (u"The Boondocks", u"http://www.gocomics.com/boondocks"), + # (u"Bottomliners", u"http://www.gocomics.com/bottomliners"), + # (u"Bound and Gagged", u"http://www.gocomics.com/boundandgagged"), + # (u"Brainwaves", u"http://www.gocomics.com/brainwaves"), + # (u"Brenda Starr", u"http://www.gocomics.com/brendastarr"), + # (u"Brewster Rockit", u"http://www.gocomics.com/brewsterrockit"), + # (u"Broom Hilda", u"http://www.gocomics.com/broomhilda"), + (u"Calvin and Hobbes", u"http://www.gocomics.com/calvinandhobbes"), + # (u"Candorville", u"http://www.gocomics.com/candorville"), + # (u"Cathy", u"http://www.gocomics.com/cathy"), + # (u"C'est la Vie", u"http://www.gocomics.com/cestlavie"), + # (u"Chuckle Bros", u"http://www.gocomics.com/chucklebros"), + # (u"Citizen Dog", u"http://www.gocomics.com/citizendog"), + # (u"The City", u"http://www.gocomics.com/thecity"), + # (u"Cleats", u"http://www.gocomics.com/cleats"), + # (u"Close to Home", u"http://www.gocomics.com/closetohome"), + # (u"Compu-toon", u"http://www.gocomics.com/compu-toon"), + # (u"Cornered", u"http://www.gocomics.com/cornered"), + (u"Cul de Sac", u"http://www.gocomics.com/culdesac"), + # (u"Daddy's Home", u"http://www.gocomics.com/daddyshome"), + # (u"Deep Cover", u"http://www.gocomics.com/deepcover"), + # (u"Dick Tracy", u"http://www.gocomics.com/dicktracy"), + # (u"The Dinette Set", u"http://www.gocomics.com/dinetteset"), + # (u"Dog Eat Doug", u"http://www.gocomics.com/dogeatdoug"), + # (u"Domestic Abuse", u"http://www.gocomics.com/domesticabuse"), + # (u"Doodles", u"http://www.gocomics.com/doodles"), + (u"Doonesbury", u"http://www.gocomics.com/doonesbury"), + # (u"The Doozies", u"http://www.gocomics.com/thedoozies"), + # (u"The Duplex", u"http://www.gocomics.com/duplex"), + # (u"Eek!", u"http://www.gocomics.com/eek"), + # (u"The Elderberries", u"http://www.gocomics.com/theelderberries"), + # (u"Flight Deck", u"http://www.gocomics.com/flightdeck"), + # (u"Flo and Friends", u"http://www.gocomics.com/floandfriends"), + # (u"The Flying McCoys", u"http://www.gocomics.com/theflyingmccoys"), + (u"For Better or For Worse", u"http://www.gocomics.com/forbetterorforworse"), + # (u"For Heaven's Sake", u"http://www.gocomics.com/forheavenssake"), + # (u"Fort Knox", u"http://www.gocomics.com/fortknox"), + # (u"FoxTrot", u"http://www.gocomics.com/foxtrot"), + (u"FoxTrot Classics", u"http://www.gocomics.com/foxtrotclassics"), + # (u"Frank & Ernest", u"http://www.gocomics.com/frankandernest"), + # (u"Fred Basset", u"http://www.gocomics.com/fredbasset"), + # (u"Free Range", u"http://www.gocomics.com/freerange"), + # (u"Frog Applause", u"http://www.gocomics.com/frogapplause"), + # (u"The Fusco Brothers", u"http://www.gocomics.com/thefuscobrothers"), + (u"Garfield", u"http://www.gocomics.com/garfield"), + # (u"Garfield Minus Garfield", u"http://www.gocomics.com/garfieldminusgarfield"), + # (u"Gasoline Alley", u"http://www.gocomics.com/gasolinealley"), + # (u"Gil Thorp", u"http://www.gocomics.com/gilthorp"), + # (u"Ginger Meggs", u"http://www.gocomics.com/gingermeggs"), + # (u"Girls & Sports", u"http://www.gocomics.com/girlsandsports"), + # (u"Haiku Ewe", u"http://www.gocomics.com/haikuewe"), + # (u"Heart of the City", u"http://www.gocomics.com/heartofthecity"), + # (u"Heathcliff", u"http://www.gocomics.com/heathcliff"), + # (u"Herb and Jamaal", u"http://www.gocomics.com/herbandjamaal"), + # (u"Home and Away", u"http://www.gocomics.com/homeandaway"), + # (u"Housebroken", u"http://www.gocomics.com/housebroken"), + # (u"Hubert and Abby", u"http://www.gocomics.com/hubertandabby"), + # (u"Imagine This", u"http://www.gocomics.com/imaginethis"), + # (u"In the Bleachers", u"http://www.gocomics.com/inthebleachers"), + # (u"In the Sticks", u"http://www.gocomics.com/inthesticks"), + # (u"Ink Pen", u"http://www.gocomics.com/inkpen"), + # (u"It's All About You", u"http://www.gocomics.com/itsallaboutyou"), + # (u"Joe Vanilla", u"http://www.gocomics.com/joevanilla"), + # (u"La Cucaracha", u"http://www.gocomics.com/lacucaracha"), + # (u"Last Kiss", u"http://www.gocomics.com/lastkiss"), + # (u"Legend of Bill", u"http://www.gocomics.com/legendofbill"), + # (u"Liberty Meadows", u"http://www.gocomics.com/libertymeadows"), + (u"Lio", u"http://www.gocomics.com/lio"), + # (u"Little Dog Lost", u"http://www.gocomics.com/littledoglost"), + # (u"Little Otto", u"http://www.gocomics.com/littleotto"), + # (u"Loose Parts", u"http://www.gocomics.com/looseparts"), + # (u"Love Is...", u"http://www.gocomics.com/loveis"), + # (u"Maintaining", u"http://www.gocomics.com/maintaining"), + # (u"The Meaning of Lila", u"http://www.gocomics.com/meaningoflila"), + # (u"Middle-Aged White Guy", u"http://www.gocomics.com/middleagedwhiteguy"), + # (u"The Middletons", u"http://www.gocomics.com/themiddletons"), + # (u"Momma", u"http://www.gocomics.com/momma"), + # (u"Mutt & Jeff", u"http://www.gocomics.com/muttandjeff"), + # (u"Mythtickle", u"http://www.gocomics.com/mythtickle"), + # (u"Nest Heads", u"http://www.gocomics.com/nestheads"), + # (u"NEUROTICA", u"http://www.gocomics.com/neurotica"), + (u"New Adventures of Queen Victoria", u"http://www.gocomics.com/thenewadventuresofqueenvictoria"), + (u"Non Sequitur", u"http://www.gocomics.com/nonsequitur"), + # (u"The Norm", u"http://www.gocomics.com/thenorm"), + # (u"On A Claire Day", u"http://www.gocomics.com/onaclaireday"), + # (u"One Big Happy", u"http://www.gocomics.com/onebighappy"), + # (u"The Other Coast", u"http://www.gocomics.com/theothercoast"), + # (u"Out of the Gene Pool Re-Runs", u"http://www.gocomics.com/outofthegenepool"), + # (u"Overboard", u"http://www.gocomics.com/overboard"), + # (u"Pibgorn", u"http://www.gocomics.com/pibgorn"), + # (u"Pibgorn Sketches", u"http://www.gocomics.com/pibgornsketches"), + (u"Pickles", u"http://www.gocomics.com/pickles"), + # (u"Pinkerton", u"http://www.gocomics.com/pinkerton"), + # (u"Pluggers", u"http://www.gocomics.com/pluggers"), + (u"Pooch Cafe", u"http://www.gocomics.com/poochcafe"), + # (u"PreTeena", u"http://www.gocomics.com/preteena"), + # (u"The Quigmans", u"http://www.gocomics.com/thequigmans"), + # (u"Rabbits Against Magic", u"http://www.gocomics.com/rabbitsagainstmagic"), + (u"Real Life Adventures", u"http://www.gocomics.com/reallifeadventures"), + # (u"Red and Rover", u"http://www.gocomics.com/redandrover"), + # (u"Red Meat", u"http://www.gocomics.com/redmeat"), + # (u"Reynolds Unwrapped", u"http://www.gocomics.com/reynoldsunwrapped"), + # (u"Ronaldinho Gaucho", u"http://www.gocomics.com/ronaldinhogaucho"), + # (u"Rubes", u"http://www.gocomics.com/rubes"), + # (u"Scary Gary", u"http://www.gocomics.com/scarygary"), + (u"Shoe", u"http://www.gocomics.com/shoe"), + # (u"Shoecabbage", u"http://www.gocomics.com/shoecabbage"), + # (u"Skin Horse", u"http://www.gocomics.com/skinhorse"), + # (u"Slowpoke", u"http://www.gocomics.com/slowpoke"), + # (u"Speed Bump", u"http://www.gocomics.com/speedbump"), + # (u"State of the Union", u"http://www.gocomics.com/stateoftheunion"), + (u"Stone Soup", u"http://www.gocomics.com/stonesoup"), + # (u"Strange Brew", u"http://www.gocomics.com/strangebrew"), + # (u"Sylvia", u"http://www.gocomics.com/sylvia"), + # (u"Tank McNamara", u"http://www.gocomics.com/tankmcnamara"), + # (u"Tiny Sepuku", u"http://www.gocomics.com/tinysepuku"), + # (u"TOBY", u"http://www.gocomics.com/toby"), + # (u"Tom the Dancing Bug", u"http://www.gocomics.com/tomthedancingbug"), + # (u"Too Much Coffee Man", u"http://www.gocomics.com/toomuchcoffeeman"), + # (u"W.T. Duck", u"http://www.gocomics.com/wtduck"), + # (u"Watch Your Head", u"http://www.gocomics.com/watchyourhead"), + # (u"Wee Pals", u"http://www.gocomics.com/weepals"), + # (u"Winnie the Pooh", u"http://www.gocomics.com/winniethepooh"), + (u"Wizard of Id", u"http://www.gocomics.com/wizardofid"), + # (u"Working It Out", u"http://www.gocomics.com/workingitout"), + # (u"Yenny", u"http://www.gocomics.com/yenny"), + # (u"Zack Hill", u"http://www.gocomics.com/zackhill"), + (u"Ziggy", u"http://www.gocomics.com/ziggy"), + ######## COMICS - EDITORIAL ######## + ("Lalo Alcaraz","http://www.gocomics.com/laloalcaraz"), + ("Nick Anderson","http://www.gocomics.com/nickanderson"), + ("Chuck Asay","http://www.gocomics.com/chuckasay"), + ("Tony Auth","http://www.gocomics.com/tonyauth"), + ("Donna Barstow","http://www.gocomics.com/donnabarstow"), + # ("Bruce Beattie","http://www.gocomics.com/brucebeattie"), + # ("Clay Bennett","http://www.gocomics.com/claybennett"), + # ("Lisa Benson","http://www.gocomics.com/lisabenson"), + # ("Steve Benson","http://www.gocomics.com/stevebenson"), + # ("Chip Bok","http://www.gocomics.com/chipbok"), + # ("Steve Breen","http://www.gocomics.com/stevebreen"), + # ("Chris Britt","http://www.gocomics.com/chrisbritt"), + # ("Stuart Carlson","http://www.gocomics.com/stuartcarlson"), + # ("Ken Catalino","http://www.gocomics.com/kencatalino"), + # ("Paul Conrad","http://www.gocomics.com/paulconrad"), + # ("Jeff Danziger","http://www.gocomics.com/jeffdanziger"), + # ("Matt Davies","http://www.gocomics.com/mattdavies"), + # ("John Deering","http://www.gocomics.com/johndeering"), + # ("Bob Gorrell","http://www.gocomics.com/bobgorrell"), + # ("Walt Handelsman","http://www.gocomics.com/walthandelsman"), + # ("Clay Jones","http://www.gocomics.com/clayjones"), + # ("Kevin Kallaugher","http://www.gocomics.com/kevinkallaugher"), + # ("Steve Kelley","http://www.gocomics.com/stevekelley"), + # ("Dick Locher","http://www.gocomics.com/dicklocher"), + # ("Chan Lowe","http://www.gocomics.com/chanlowe"), + # ("Mike Luckovich","http://www.gocomics.com/mikeluckovich"), + # ("Gary Markstein","http://www.gocomics.com/garymarkstein"), + # ("Glenn McCoy","http://www.gocomics.com/glennmccoy"), + # ("Jim Morin","http://www.gocomics.com/jimmorin"), + # ("Jack Ohman","http://www.gocomics.com/jackohman"), + # ("Pat Oliphant","http://www.gocomics.com/patoliphant"), + # ("Joel Pett","http://www.gocomics.com/joelpett"), + # ("Ted Rall","http://www.gocomics.com/tedrall"), + # ("Michael Ramirez","http://www.gocomics.com/michaelramirez"), + # ("Marshall Ramsey","http://www.gocomics.com/marshallramsey"), + # ("Steve Sack","http://www.gocomics.com/stevesack"), + # ("Ben Sargent","http://www.gocomics.com/bensargent"), + # ("Drew Sheneman","http://www.gocomics.com/drewsheneman"), + # ("John Sherffius","http://www.gocomics.com/johnsherffius"), + # ("Small World","http://www.gocomics.com/smallworld"), + # ("Scott Stantis","http://www.gocomics.com/scottstantis"), + # ("Wayne Stayskal","http://www.gocomics.com/waynestayskal"), + # ("Dana Summers","http://www.gocomics.com/danasummers"), + # ("Paul Szep","http://www.gocomics.com/paulszep"), + # ("Mike Thompson","http://www.gocomics.com/mikethompson"), + # ("Tom Toles","http://www.gocomics.com/tomtoles"), + # ("Gary Varvel","http://www.gocomics.com/garyvarvel"), + # ("ViewsAfrica","http://www.gocomics.com/viewsafrica"), + # ("ViewsAmerica","http://www.gocomics.com/viewsamerica"), + # ("ViewsAsia","http://www.gocomics.com/viewsasia"), + # ("ViewsBusiness","http://www.gocomics.com/viewsbusiness"), + # ("ViewsEurope","http://www.gocomics.com/viewseurope"), + # ("ViewsLatinAmerica","http://www.gocomics.com/viewslatinamerica"), + # ("ViewsMidEast","http://www.gocomics.com/viewsmideast"), + # ("Views of the World","http://www.gocomics.com/viewsoftheworld"), + # ("Kerry Waghorn","http://www.gocomics.com/facesinthenews"), + # ("Dan Wasserman","http://www.gocomics.com/danwasserman"), + # ("Signe Wilkinson","http://www.gocomics.com/signewilkinson"), + # ("Wit of the World","http://www.gocomics.com/witoftheworld"), + # ("Don Wright","http://www.gocomics.com/donwright"), + ]: + articles = self.make_links(url) + if articles: + feeds.append((title, articles)) + return feeds + + def make_links(self, url): + title = 'Temp' + current_articles = [] + pages = range(1, self.num_comics_to_get+1) + for page in pages: + page_soup = self.index_to_soup(url) + if page_soup: + try: + strip_title = page_soup.h1.a.string + except: + strip_title = 'Error - no page_soup.h1.a.string' + try: + date_title = page_soup.find('ul', attrs={'class': 'feature-nav'}).li.string + except: + date_title = 'Error - no page_soup.h1.li.string' + title = strip_title + ' - ' + date_title + for i in range(2): + try: + strip_url_date = page_soup.h1.a['href'] + break #success - this is normal exit + except: + continue #try to get strip_url_date again + continue # give up on this strip date + for i in range(2): + try: + prev_strip_url_date = page_soup.find('a', attrs={'class': 'prev'})['href'] + break #success - this is normal exit + except: + continue #try to get prev_strip_url_date again + continue # give up on this prev strip date + if strip_url_date: + page_url = 'http://www.gocomics.com' + strip_url_date + else: + continue + if prev_strip_url_date: + prev_page_url = 'http://www.gocomics.com' + prev_strip_url_date + else: + continue + current_articles.append({'title': title, 'url': page_url, 'description':'', 'date':''}) + url = prev_page_url + current_articles.reverse() + return current_articles + + def preprocess_html(self, soup): + if soup.title: + title_string = soup.title.string.strip() + _cd = title_string.split(',',1)[1] + comic_date = ' '.join(_cd.split(' ', 4)[0:-1]) + if soup.h1.span: + artist = soup.h1.span.string + soup.h1.span.string.replaceWith(comic_date + artist) + feature_item = soup.find('p',attrs={'class':'feature_item'}) + if feature_item.a: + a_tag = feature_item.a + a_href = a_tag["href"] + img_tag = a_tag.img + img_tag["src"] = a_href + img_tag["width"] = self.comic_size + img_tag["height"] = None + return self.adeify_images(soup) + + extra_css = ''' + h1{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;} + h2{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;} + img {max-width:100%; min-width:100%;} + p{font-family:Arial,Helvetica,sans-serif;font-size:small;} + body{font-family:Helvetica,Arial,sans-serif;font-size:small;} + ''' From 926e3fa5459363b0ba5e9e14c00abe5e0c4cb08a Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 14 Aug 2010 12:19:14 -0600 Subject: [PATCH 03/73] Update podofo in windows build to -r1269 --- setup/installer/windows/notes.rst | 24 +++-------------- src/calibre/trac/bzr_commit_plugin.py | 38 +++++++++++++-------------- 2 files changed, 22 insertions(+), 40 deletions(-) diff --git a/setup/installer/windows/notes.rst b/setup/installer/windows/notes.rst index e7fef2be80..8f1cc80bb5 100644 --- a/setup/installer/windows/notes.rst +++ b/setup/installer/windows/notes.rst @@ -237,28 +237,10 @@ cp build/podofo/build/src/Release/podofo.exp lib/ cp build/podofo/build/podofo_config.h include/podofo/ cp -r build/podofo/src/* include/podofo/ -The following patch (against 0.8.1) was required to get it to compile: +You have to use >0.8.1 (>= revision 1269) + +The following patch (against -r1269) was required to get it to compile: -Index: src/PdfImage.cpp -=================================================================== ---- src/PdfImage.cpp (revision 1261) -+++ src/PdfImage.cpp (working copy) -@@ -627,7 +627,7 @@ - - long lLen = static_cast(pInfo->rowbytes * height); - char* pBuffer = static_cast(malloc(sizeof(char) * lLen)); -- png_bytep pRows[height]; -+ png_bytepp pRows = static_cast(malloc(sizeof(png_bytep)*height)); - for(int y=0; y(pBuffer + (y * pInfo->rowbytes)); -@@ -672,6 +672,7 @@ - this->SetImageData( width, height, pInfo->bit_depth, &stream ); - - free(pBuffer); -+ free(pRows); - } - #endif // PODOFO_HAVE_PNG_LIB Index: src/PdfFiltersPrivate.cpp =================================================================== diff --git a/src/calibre/trac/bzr_commit_plugin.py b/src/calibre/trac/bzr_commit_plugin.py index 5ec6e8f7e8..c39131cccb 100644 --- a/src/calibre/trac/bzr_commit_plugin.py +++ b/src/calibre/trac/bzr_commit_plugin.py @@ -26,18 +26,18 @@ import bzrlib class cmd_commit(_cmd_commit): - + @classmethod def trac_url(self, username, password, url): - return url.replace('//', '//%s:%s@'%(username, password))+'/login/xmlrpc' - + return url.replace('//', '//%s:%s@'%(username, password))+'/login/xmlrpc' + def get_trac_summary(self, bug, url): print 'Getting bug summary for bug #%s'%bug, server = xmlrpclib.ServerProxy(url) attributes = server.ticket.get(int(bug))[-1] print attributes['summary'] return attributes['summary'] - + def expand_bug(self, msg, nick, config, bug_tracker, type='trac'): prefix = '%s_%s_'%(type, nick) username = config.get_user_option(prefix+'username') @@ -55,10 +55,10 @@ class cmd_commit(_cmd_commit): url = self.trac_url(username, password, bug_tracker) summary = self.get_trac_summary(bug, url) if summary: - msg = msg.replace('#%s'%bug, '#%s (%s)'%(bug, summary)) + msg = msg.replace('#%s'%bug, '#%s (%s)'%(bug, summary)) return msg, bug, url, action - - + + def get_bugtracker(self, basedir, type='trac'): config = os.path.join(basedir, '.bzr', 'branch', 'branch.conf') bugtracker, nick = None, None @@ -69,16 +69,16 @@ class cmd_commit(_cmd_commit): nick, bugtracker = match.group(1), match.group(2) break return nick, bugtracker - + def expand_message(self, msg, tree): nick, bugtracker = self.get_bugtracker(tree.basedir, type='trac') if not bugtracker: return msg config = branch.Branch.open(tree.basedir).get_config() msg, bug, url, action = self.expand_bug(msg, nick, config, bugtracker) - + return msg, bug, url, action, nick, config - + def run(self, message=None, file=None, verbose=False, selected_list=None, unchanged=False, strict=False, local=False, fixes=None, author=None, show_diff=False, exclude=None): @@ -89,18 +89,18 @@ class cmd_commit(_cmd_commit): self.expand_message(message, tree_files(selected_list)[0]) except ValueError: pass - + if nick and bug and not fixes: fixes = [nick+':'+bug] - - ret = _cmd_commit.run(self, message=message, file=file, verbose=verbose, + + ret = _cmd_commit.run(self, message=message, file=file, verbose=verbose, selected_list=selected_list, unchanged=unchanged, - strict=strict, local=local, fixes=fixes, + strict=strict, local=local, fixes=fixes, author=author, show_diff=show_diff, exclude=exclude) if message and bug and action and nick and config: self.close_bug(bug, action, url, config) return ret - + def close_bug(self, bug, action, url, config): print 'Closing bug #%s'% bug nick = config.get_nickname() @@ -110,8 +110,8 @@ class cmd_commit(_cmd_commit): action = action+'ed' msg = '%s in branch %s. %s'%(action, nick, suffix) server = xmlrpclib.ServerProxy(url) - server.ticket.update(int(bug), msg, - {'status':'closed', 'resolution':'fixed'}, + server.ticket.update(int(bug), msg, + {'status':'closed', 'resolution':'fixed'}, False) - -bzrlib.commands.register_command(cmd_commit) \ No newline at end of file + +bzrlib.commands.register_command(cmd_commit) From fa7f05ec41b1f79a8b1a4fbaf61d2dbb32a7aa41 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 14 Aug 2010 13:11:21 -0600 Subject: [PATCH 04/73] Parallel jobs can now specify how many cores they use. Have comic conversion jobs specify they use all cores --- src/calibre/customize/conversion.py | 4 ++++ src/calibre/ebooks/comic/input.py | 1 + src/calibre/gui2/actions/convert.py | 13 ++++++++++++- src/calibre/gui2/jobs.py | 3 ++- src/calibre/utils/ipc/job.py | 1 + 5 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/calibre/customize/conversion.py b/src/calibre/customize/conversion.py index e98f34273f..72c067747d 100644 --- a/src/calibre/customize/conversion.py +++ b/src/calibre/customize/conversion.py @@ -112,6 +112,10 @@ class InputFormatPlugin(Plugin): #: convenience method, :meth:`get_image_collection`. is_image_collection = False + #: Number of CPU cores used by this plugin + #: A value of -1 means that it uses all available cores + core_usage = 1 + #: If set to True, the input plugin will perform special processing #: to make its output suitable for viewing for_viewer = False diff --git a/src/calibre/ebooks/comic/input.py b/src/calibre/ebooks/comic/input.py index bfc6ff30ca..443a302999 100755 --- a/src/calibre/ebooks/comic/input.py +++ b/src/calibre/ebooks/comic/input.py @@ -247,6 +247,7 @@ class ComicInput(InputFormatPlugin): description = 'Optimize comic files (.cbz, .cbr, .cbc) for viewing on portable devices' file_types = set(['cbz', 'cbr', 'cbc']) is_image_collection = True + core_usage = -1 options = set([ OptionRecommendation(name='colors', recommended_value=256, diff --git a/src/calibre/gui2/actions/convert.py b/src/calibre/gui2/actions/convert.py index 0641cc6a97..ef8a8688c5 100644 --- a/src/calibre/gui2/actions/convert.py +++ b/src/calibre/gui2/actions/convert.py @@ -14,6 +14,7 @@ from calibre.gui2 import error_dialog, Dispatcher from calibre.gui2.tools import convert_single_ebook, convert_bulk_ebook from calibre.utils.config import prefs from calibre.gui2.actions import InterfaceAction +from calibre.customize.ui import plugin_for_input_format class ConvertAction(InterfaceAction): @@ -115,9 +116,19 @@ class ConvertAction(InterfaceAction): def queue_convert_jobs(self, jobs, changed, bad, rows, previous, converted_func, extra_job_args=[]): for func, args, desc, fmt, id, temp_files in jobs: + input_file = args[0] + input_fmt = os.path.splitext(input_file)[1] + core_usage = 1 + if input_fmt: + input_fmt = input_fmt[1:] + plugin = plugin_for_input_format(input_fmt) + if plugin is not None: + core_usage = plugin.core_usage + if id not in bad: job = self.gui.job_manager.run_job(Dispatcher(converted_func), - func, args=args, description=desc) + func, args=args, description=desc, + core_usage=core_usage) args = [temp_files, fmt, id]+extra_job_args self.conversion_jobs[job] = tuple(args) diff --git a/src/calibre/gui2/jobs.py b/src/calibre/gui2/jobs.py index d512409d25..813a9bda45 100644 --- a/src/calibre/gui2/jobs.py +++ b/src/calibre/gui2/jobs.py @@ -198,8 +198,9 @@ class JobManager(QAbstractTableModel): return False def run_job(self, done, name, args=[], kwargs={}, - description=''): + description='', core_usage=1): job = ParallelJob(name, description, done, args=args, kwargs=kwargs) + job.core_usage = core_usage self.add_job(job) self.server.add_job(job) return job diff --git a/src/calibre/utils/ipc/job.py b/src/calibre/utils/ipc/job.py index a6c39ffc6b..91db333791 100644 --- a/src/calibre/utils/ipc/job.py +++ b/src/calibre/utils/ipc/job.py @@ -42,6 +42,7 @@ class BaseJob(object): self._message = None self._status_text = _('Waiting...') self._done_called = False + self.core_usage = 1 def update(self, consume_notifications=True): if self.duration is not None: From 03dc0060aa335c029d7ac4ec33e7aa63b3b8ef9f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 14 Aug 2010 13:29:53 -0600 Subject: [PATCH 05/73] When converting comics, run only a single conversion job at a time, since comic conversion uses all CPU cores anyway --- src/calibre/utils/ipc/server.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/calibre/utils/ipc/server.py b/src/calibre/utils/ipc/server.py index 38d465cb78..6ac8df7a24 100644 --- a/src/calibre/utils/ipc/server.py +++ b/src/calibre/utils/ipc/server.py @@ -103,7 +103,7 @@ class Server(Thread): authkey=self.auth_key, backlog=4) self.add_jobs_queue, self.changed_jobs_queue = Queue(), Queue() self.kill_queue = Queue() - self.waiting_jobs, self.processing_jobs = deque(), deque() + self.waiting_jobs, self.processing_jobs = [], deque() self.pool, self.workers = deque(), deque() self.launched_worker_count = 0 self._worker_launch_lock = RLock() @@ -167,7 +167,7 @@ class Server(Thread): job = self.add_jobs_queue.get(True, 0.2) if job is None: break - self.waiting_jobs.append(job) + self.waiting_jobs.insert(0, job) except Empty: pass @@ -202,8 +202,9 @@ class Server(Thread): pass # Start waiting jobs - if len(self.pool) > 0 and len(self.waiting_jobs) > 0: - job = self.waiting_jobs.pop() + sj = self.suitable_waiting_job() + if sj is not None: + job = self.waiting_jobs.pop(sj) job.start_time = time.time() if job.kill_on_start: job.duration = 0.0 @@ -224,6 +225,17 @@ class Server(Thread): except Empty: break + def suitable_waiting_job(self): + available_workers = len(self.pool) + if available_workers == 0: + return None + for i, job in enumerate(self.waiting_jobs): + if job.core_usage == -1: + if available_workers >= self.pool_size: + return i + elif job.core_usage <= available_workers: + return i + def kill_job(self, job): self.kill_queue.put(job) From 348d4e860b196b9f06836a547f0ccdeafb25a9a9 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 14 Aug 2010 13:52:42 -0600 Subject: [PATCH 06/73] ... --- src/calibre/gui2/wizard/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/wizard/__init__.py b/src/calibre/gui2/wizard/__init__.py index 854733a653..394b919016 100644 --- a/src/calibre/gui2/wizard/__init__.py +++ b/src/calibre/gui2/wizard/__init__.py @@ -61,7 +61,7 @@ class Kindle(Device): output_profile = 'kindle' output_format = 'MOBI' - name = 'Kindle 1 or 2' + name = 'Kindle 1, 2 or 3' manufacturer = 'Amazon' id = 'kindle' From 9eb7e3d53f0bf69843a9efef845b72b04e2258bc Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 14 Aug 2010 15:18:06 -0600 Subject: [PATCH 07/73] Allow even quicker switching of libraries, via drop down menu on the Choose Library button --- src/calibre/gui2/actions/choose_library.py | 108 +++++++++++++++++++++ src/calibre/gui2/ui.py | 2 + 2 files changed, 110 insertions(+) diff --git a/src/calibre/gui2/actions/choose_library.py b/src/calibre/gui2/actions/choose_library.py index 30e2f79424..bc22c93c27 100644 --- a/src/calibre/gui2/actions/choose_library.py +++ b/src/calibre/gui2/actions/choose_library.py @@ -5,8 +5,64 @@ __license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' +import os +from functools import partial + +from PyQt4.Qt import QMenu + +from calibre import isbytestring +from calibre.constants import filesystem_encoding +from calibre.utils.config import prefs +from calibre.gui2 import gprefs from calibre.gui2.actions import InterfaceAction +class LibraryUsageStats(object): + + def __init__(self): + self.stats = {} + self.read_stats() + + def read_stats(self): + stats = gprefs.get('library_usage_stats', {}) + self.stats = stats + + def write_stats(self): + locs = list(self.stats.keys()) + locs.sort(cmp=lambda x, y: cmp(self.stats[x], self.stats[y]), + reverse=True) + for key in locs[15:]: + self.stats.pop(key) + gprefs.set('library_usage_stats', self.stats) + + def canonicalize_path(self, lpath): + if isbytestring(lpath): + lpath = lpath.decode(filesystem_encoding) + lpath = lpath.replace(os.sep, '/') + return lpath + + def library_used(self, db): + lpath = self.canonicalize_path(db.library_path) + if lpath not in self.stats: + self.stats[lpath] = 0 + self.stats[lpath] += 1 + self.write_stats() + + def locations(self, db): + lpath = self.canonicalize_path(db.library_path) + locs = list(self.stats.keys()) + if lpath in locs: + locs.remove(lpath) + locs.sort(cmp=lambda x, y: cmp(self.stats[x], self.stats[y]), + reverse=True) + for loc in locs: + yield self.pretty(loc), loc + + def pretty(self, loc): + if loc.endswith('/'): + loc = loc[:-1] + return loc.split('/')[-1] + + class ChooseLibraryAction(InterfaceAction): name = 'Choose Library' @@ -17,10 +73,62 @@ class ChooseLibraryAction(InterfaceAction): self.count_changed(0) self.qaction.triggered.connect(self.choose_library) + self.stats = LibraryUsageStats() + self.create_action(spec=(_('Switch to library...'), 'lt.png', None, + None), attr='action_choose') + self.action_choose.triggered.connect(self.choose_library) + self.choose_menu = QMenu(self.gui) + self.choose_menu.addAction(self.action_choose) + self.qaction.setMenu(self.choose_menu) + + self.quick_menu = QMenu(_('Quick switch')) + self.quick_menu_action = self.choose_menu.addMenu(self.quick_menu) + self.qs_separator = self.choose_menu.addSeparator() + self.switch_actions = [] + for i in range(5): + ac = self.create_action(spec=('', None, None, None), + attr='switch_action%d'%i) + self.switch_actions.append(ac) + ac.setVisible(False) + ac.triggered.connect(partial(self.qs_requested, i)) + self.choose_menu.addAction(ac) + + def library_used(self, db): + self.stats.library_used(db) + self.build_menus() + + def build_menus(self): + db = self.gui.library_view.model().db + locations = list(self.stats.locations(db)) + for ac in self.switch_actions: + ac.setVisible(False) + self.quick_menu.clear() + self.qs_locations = [i[1] for i in locations] + for name, loc in locations: + self.quick_menu.addAction(name, partial(self.switch_requested, + loc)) + + for i, x in enumerate(locations[:len(self.switch_actions)]): + name, loc = x + ac = self.switch_actions[i] + ac.setText(name) + ac.setVisible(True) + + self.quick_menu_action.setVisible(bool(locations)) + + def location_selected(self, loc): enabled = loc == 'library' self.qaction.setEnabled(enabled) + def switch_requested(self, location): + loc = location.replace('/', os.sep) + prefs['library_path'] = loc + self.gui.library_moved(loc) + + def qs_requested(self, idx, *args): + self.switch_requested(self.qs_locations[idx]) + def count_changed(self, new_count): text = self.action_spec[0]%new_count a = self.qaction diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 9e4b4f3865..820656d0e2 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -251,6 +251,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{ self.donate_button.start_animation() self.iactions['Fetch News'].connect_scheduler() + self.iactions['Choose Library'].library_used(self.library_view.model().db) def start_content_server(self): from calibre.library.server.main import start_threaded_server @@ -368,6 +369,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{ self.library_view.model().count_changed() self.iactions['Fetch News'].database_changed(db) prefs['library_path'] = self.library_path + self.iactions['Choose Library'].library_used(self.library_view.model().db) def location_selected(self, location): From 3e410d370b5daf99b33d28a3c6a95e2c4c2c04c1 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 14 Aug 2010 15:30:33 -0600 Subject: [PATCH 08/73] Fix regression that broke reading covers from CBR files --- src/calibre/libunrar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/libunrar.py b/src/calibre/libunrar.py index 2f44dcdf4e..5dc66eaa36 100644 --- a/src/calibre/libunrar.py +++ b/src/calibre/libunrar.py @@ -239,7 +239,7 @@ def _extract_member(path, match, name): PFCode = _libunrar.RARProcessFileW(arc_data, RAR_EXTRACT, None, None) if PFCode != 0: raise UnRARException(_interpret_process_file_error(PFCode)) - abspath = os.path.abspath(*file_name.split('/')) + abspath = os.path.abspath(os.path.join(*file_name.split('/'))) return abspath else: PFCode = _libunrar.RARProcessFileW(arc_data, RAR_SKIP, None, None) From 0a78354bd57893df9f926850aa736d6b434cd40b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 14 Aug 2010 19:38:44 -0600 Subject: [PATCH 09/73] Workaround for PyQt4/util-linux conflict on gentoo --- src/calibre/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/calibre/__init__.py b/src/calibre/__init__.py index 34801d39ca..16aaab73dd 100644 --- a/src/calibre/__init__.py +++ b/src/calibre/__init__.py @@ -3,7 +3,7 @@ __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import sys, os, re, logging, time, mimetypes, \ +import uuid, sys, os, re, logging, time, mimetypes, \ __builtin__, warnings, multiprocessing from urllib import getproxies __builtin__.__dict__['dynamic_property'] = lambda(func): func(None) @@ -23,6 +23,8 @@ from calibre.startup import winutil, winutilerror import mechanize +uuid.uuid4() # Imported before PyQt4 to workaround PyQt4 util-linux conflict on gentoo + if False: winutil, winutilerror, __appname__, islinux, __version__ fcntl, win32event, isfrozen, __author__, terminal_controller From a81bb5737a08afd22e3bb62a1c90719099519434 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 14 Aug 2010 22:34:08 -0600 Subject: [PATCH 10/73] Android driver: Send books to Aldiko in preference to WordPlayer by default --- src/calibre/devices/android/driver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py index 8b7f5f3d24..951ed66e72 100644 --- a/src/calibre/devices/android/driver.py +++ b/src/calibre/devices/android/driver.py @@ -46,7 +46,7 @@ class ANDROID(USBMS): # Eken? 0x040d : { 0x0851 : [0x0001]}, } - EBOOK_DIR_MAIN = ['wordplayer/calibretransfer', 'eBooks/import', 'Books'] + EBOOK_DIR_MAIN = ['eBooks/import', 'wordplayer/calibretransfer', 'Books'] EXTRA_CUSTOMIZATION_MESSAGE = _('Comma separated list of directories to ' 'send e-books to on the device. The first one that exists will ' 'be used') From 3b3cc6959fbff21020476b280439b9db6ae00ba1 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 14 Aug 2010 22:41:00 -0600 Subject: [PATCH 11/73] Fix Comin Input default settings not being used when bulk converting comics --- src/calibre/gui2/convert/__init__.py | 27 ++++++++++++++++--- src/calibre/gui2/convert/comic_input.py | 3 ++- src/calibre/gui2/convert/debug.py | 3 ++- src/calibre/gui2/convert/epub_output.py | 3 ++- src/calibre/gui2/convert/fb2_input.py | 3 ++- src/calibre/gui2/convert/fb2_output.py | 3 ++- src/calibre/gui2/convert/look_and_feel.py | 3 ++- src/calibre/gui2/convert/lrf_output.py | 3 ++- src/calibre/gui2/convert/metadata.py | 3 ++- src/calibre/gui2/convert/mobi_output.py | 4 +-- src/calibre/gui2/convert/page_setup.py | 3 ++- src/calibre/gui2/convert/pdb_input.py | 3 ++- src/calibre/gui2/convert/pdb_output.py | 3 ++- src/calibre/gui2/convert/pdf_input.py | 3 ++- src/calibre/gui2/convert/pdf_output.py | 3 ++- src/calibre/gui2/convert/rb_output.py | 3 ++- .../gui2/convert/structure_detection.py | 3 ++- src/calibre/gui2/convert/toc.py | 3 ++- src/calibre/gui2/convert/txt_input.py | 3 ++- src/calibre/gui2/convert/txt_output.py | 3 ++- src/calibre/gui2/dialogs/config/__init__.py | 10 +++---- src/calibre/gui2/tools.py | 4 +-- 22 files changed, 68 insertions(+), 31 deletions(-) diff --git a/src/calibre/gui2/convert/__init__.py b/src/calibre/gui2/convert/__init__.py index 437741d685..89cd372fad 100644 --- a/src/calibre/gui2/convert/__init__.py +++ b/src/calibre/gui2/convert/__init__.py @@ -16,18 +16,39 @@ from calibre.ebooks.conversion.config import load_defaults, \ save_defaults as save_defaults_, \ load_specifics, GuiRecommendations from calibre import prepare_string_for_xml +from calibre.customize.ui import plugin_for_input_format + +def config_widget_for_input_plugin(plugin): + name = plugin.name.lower().replace(' ', '_') + try: + return __import__('calibre.gui2.convert.'+name, + fromlist=[1]) + except ImportError: + pass + +def bulk_defaults_for_input_format(fmt): + plugin = plugin_for_input_format(fmt) + if plugin is not None: + w = config_widget_for_input_plugin(plugin) + if w is not None: + return load_defaults(w.COMMIT_NAME) + return {} + + class Widget(QWidget): TITLE = _('Unknown') ICON = I('config.svg') HELP = '' + COMMIT_NAME = None - def __init__(self, parent, name, options): + def __init__(self, parent, options): QWidget.__init__(self, parent) self.setupUi(self) self._options = options - self._name = name + self._name = self.commit_name = self.COMMIT_NAME + assert self._name is not None self._icon = QIcon(self.ICON) for name in self._options: if not hasattr(self, 'opt_'+name): @@ -58,7 +79,7 @@ class Widget(QWidget): def commit_options(self, save_defaults=False): recs = self.create_recommendations() if save_defaults: - save_defaults_(self._name, recs) + save_defaults_(self.commit_name, recs) return recs def create_recommendations(self): diff --git a/src/calibre/gui2/convert/comic_input.py b/src/calibre/gui2/convert/comic_input.py index 25c39fe138..c2b6bfffaa 100644 --- a/src/calibre/gui2/convert/comic_input.py +++ b/src/calibre/gui2/convert/comic_input.py @@ -14,9 +14,10 @@ class PluginWidget(Widget, Ui_Form): TITLE = _('Comic Input') HELP = _('Options specific to')+' comic '+_('input') + COMMIT_NAME = 'comic_input' def __init__(self, parent, get_option, get_help, db=None, book_id=None): - Widget.__init__(self, parent, 'comic_input', + Widget.__init__(self, parent, ['colors', 'dont_normalize', 'keep_aspect_ratio', 'right2left', 'despeckle', 'no_sort', 'no_process', 'landscape', 'dont_sharpen', 'disable_trim', 'wide', 'output_format', diff --git a/src/calibre/gui2/convert/debug.py b/src/calibre/gui2/convert/debug.py index 78fac42df8..6fd1975443 100644 --- a/src/calibre/gui2/convert/debug.py +++ b/src/calibre/gui2/convert/debug.py @@ -19,9 +19,10 @@ class DebugWidget(Widget, Ui_Form): TITLE = _('Debug') ICON = I('debug.svg') HELP = _('Debug the conversion process.') + COMMIT_NAME = 'debug' def __init__(self, parent, get_option, get_help, db=None, book_id=None): - Widget.__init__(self, parent, 'debug', + Widget.__init__(self, parent, ['debug_pipeline'] ) self.db, self.book_id = db, book_id diff --git a/src/calibre/gui2/convert/epub_output.py b/src/calibre/gui2/convert/epub_output.py index 8130b00273..2d1fcdfcd8 100644 --- a/src/calibre/gui2/convert/epub_output.py +++ b/src/calibre/gui2/convert/epub_output.py @@ -14,9 +14,10 @@ class PluginWidget(Widget, Ui_Form): TITLE = _('EPUB Output') HELP = _('Options specific to')+' EPUB '+_('output') + COMMIT_NAME = 'epub_output' def __init__(self, parent, get_option, get_help, db=None, book_id=None): - Widget.__init__(self, parent, 'epub_output', + Widget.__init__(self, parent, ['dont_split_on_page_breaks', 'flow_size', 'no_default_epub_cover', 'no_svg_cover', 'preserve_cover_aspect_ratio',] diff --git a/src/calibre/gui2/convert/fb2_input.py b/src/calibre/gui2/convert/fb2_input.py index 6f950e7663..702f87754f 100644 --- a/src/calibre/gui2/convert/fb2_input.py +++ b/src/calibre/gui2/convert/fb2_input.py @@ -11,9 +11,10 @@ class PluginWidget(Widget, Ui_Form): TITLE = _('FB2 Input') HELP = _('Options specific to')+' FB2 '+_('input') + COMMIT_NAME = 'fb2_input' def __init__(self, parent, get_option, get_help, db=None, book_id=None): - Widget.__init__(self, parent, 'fb2_input', + Widget.__init__(self, parent, ['no_inline_fb2_toc']) self.db, self.book_id = db, book_id self.initialize_options(get_option, get_help, db, book_id) diff --git a/src/calibre/gui2/convert/fb2_output.py b/src/calibre/gui2/convert/fb2_output.py index 76d4c56af3..55c708c2a4 100644 --- a/src/calibre/gui2/convert/fb2_output.py +++ b/src/calibre/gui2/convert/fb2_output.py @@ -13,8 +13,9 @@ class PluginWidget(Widget, Ui_Form): TITLE = _('FB2 Output') HELP = _('Options specific to')+' FB2 '+_('output') + COMMIT_NAME = 'fb2_output' def __init__(self, parent, get_option, get_help, db=None, book_id=None): - Widget.__init__(self, parent, 'fb2_output', ['inline_toc']) + Widget.__init__(self, parent, ['inline_toc']) self.db, self.book_id = db, book_id self.initialize_options(get_option, get_help, db, book_id) diff --git a/src/calibre/gui2/convert/look_and_feel.py b/src/calibre/gui2/convert/look_and_feel.py index 793d170a13..c5f579fc9e 100644 --- a/src/calibre/gui2/convert/look_and_feel.py +++ b/src/calibre/gui2/convert/look_and_feel.py @@ -16,9 +16,10 @@ class LookAndFeelWidget(Widget, Ui_Form): TITLE = _('Look & Feel') ICON = I('lookfeel.svg') HELP = _('Control the look and feel of the output') + COMMIT_NAME = 'look_and_feel' def __init__(self, parent, get_option, get_help, db=None, book_id=None): - Widget.__init__(self, parent, 'look_and_feel', + Widget.__init__(self, parent, ['change_justification', 'extra_css', 'base_font_size', 'font_size_mapping', 'line_height', 'linearize_tables', diff --git a/src/calibre/gui2/convert/lrf_output.py b/src/calibre/gui2/convert/lrf_output.py index b0ddc3dadb..1b4f1acad3 100644 --- a/src/calibre/gui2/convert/lrf_output.py +++ b/src/calibre/gui2/convert/lrf_output.py @@ -18,9 +18,10 @@ class PluginWidget(Widget, Ui_Form): TITLE = _('LRF Output') HELP = _('Options specific to')+' LRF '+_('output') + COMMIT_NAME = 'lrf_output' def __init__(self, parent, get_option, get_help, db=None, book_id=None): - Widget.__init__(self, parent, 'lrf_output', + Widget.__init__(self, parent, ['wordspace', 'header', 'header_format', 'minimum_indent', 'serif_family', 'render_tables_as_images', 'sans_family', 'mono_family', diff --git a/src/calibre/gui2/convert/metadata.py b/src/calibre/gui2/convert/metadata.py index cb5b99c525..daabd6226a 100644 --- a/src/calibre/gui2/convert/metadata.py +++ b/src/calibre/gui2/convert/metadata.py @@ -42,9 +42,10 @@ class MetadataWidget(Widget, Ui_Form): ICON = I('dialog_information.svg') HELP = _('Set the metadata. The output file will contain as much of this ' 'metadata as possible.') + COMMIT_NAME = 'metadata' def __init__(self, parent, get_option, get_help, db=None, book_id=None): - Widget.__init__(self, parent, 'metadata', ['prefer_metadata_cover']) + Widget.__init__(self, parent, ['prefer_metadata_cover']) self.db, self.book_id = db, book_id self.cover_changed = False self.cover_data = None diff --git a/src/calibre/gui2/convert/mobi_output.py b/src/calibre/gui2/convert/mobi_output.py index 396078c48a..7eff3b55a2 100644 --- a/src/calibre/gui2/convert/mobi_output.py +++ b/src/calibre/gui2/convert/mobi_output.py @@ -19,10 +19,10 @@ class PluginWidget(Widget, Ui_Form): TITLE = _('MOBI Output') HELP = _('Options specific to')+' MOBI '+_('output') - + COMMIT_NAME = 'mobi_output' def __init__(self, parent, get_option, get_help, db=None, book_id=None): - Widget.__init__(self, parent, 'mobi_output', + Widget.__init__(self, parent, ['prefer_author_sort', 'rescale_images', 'toc_title', 'dont_compress', 'no_inline_toc', 'masthead_font','personal_doc'] ) diff --git a/src/calibre/gui2/convert/page_setup.py b/src/calibre/gui2/convert/page_setup.py index 9faaf04ecc..e824c18b57 100644 --- a/src/calibre/gui2/convert/page_setup.py +++ b/src/calibre/gui2/convert/page_setup.py @@ -33,9 +33,10 @@ class ProfileModel(QAbstractListModel): class PageSetupWidget(Widget, Ui_Form): TITLE = _('Page Setup') + COMMIT_NAME = 'page_setup' def __init__(self, parent, get_option, get_help, db=None, book_id=None): - Widget.__init__(self, parent, 'page_setup', + Widget.__init__(self, parent, ['margin_top', 'margin_left', 'margin_right', 'margin_bottom', 'input_profile', 'output_profile'] ) diff --git a/src/calibre/gui2/convert/pdb_input.py b/src/calibre/gui2/convert/pdb_input.py index 058f589856..6adc49c3ed 100644 --- a/src/calibre/gui2/convert/pdb_input.py +++ b/src/calibre/gui2/convert/pdb_input.py @@ -11,9 +11,10 @@ class PluginWidget(Widget, Ui_Form): TITLE = _('PDB Input') HELP = _('Options specific to')+' PDB '+_('input') + COMMIT_NAME = 'pdb_input' def __init__(self, parent, get_option, get_help, db=None, book_id=None): - Widget.__init__(self, parent, 'pdb_input', + Widget.__init__(self, parent, ['single_line_paras', 'print_formatted_paras']) self.db, self.book_id = db, book_id self.initialize_options(get_option, get_help, db, book_id) diff --git a/src/calibre/gui2/convert/pdb_output.py b/src/calibre/gui2/convert/pdb_output.py index 220311546c..3f9d9ad3dc 100644 --- a/src/calibre/gui2/convert/pdb_output.py +++ b/src/calibre/gui2/convert/pdb_output.py @@ -15,9 +15,10 @@ class PluginWidget(Widget, Ui_Form): TITLE = _('PDB Output') HELP = _('Options specific to')+' PDB '+_('output') + COMMIT_NAME = 'pdb_output' def __init__(self, parent, get_option, get_help, db=None, book_id=None): - Widget.__init__(self, parent, 'pdb_output', ['format', 'inline_toc']) + Widget.__init__(self, parent, ['format', 'inline_toc']) self.db, self.book_id = db, book_id self.initialize_options(get_option, get_help, db, book_id) diff --git a/src/calibre/gui2/convert/pdf_input.py b/src/calibre/gui2/convert/pdf_input.py index a8cacaa5d9..511091ae85 100644 --- a/src/calibre/gui2/convert/pdf_input.py +++ b/src/calibre/gui2/convert/pdf_input.py @@ -11,9 +11,10 @@ class PluginWidget(Widget, Ui_Form): TITLE = _('PDF Input') HELP = _('Options specific to')+' PDF '+_('input') + COMMIT_NAME = 'pdf_input' def __init__(self, parent, get_option, get_help, db=None, book_id=None): - Widget.__init__(self, parent, 'pdf_input', + Widget.__init__(self, parent, ['no_images', 'unwrap_factor']) self.db, self.book_id = db, book_id self.initialize_options(get_option, get_help, db, book_id) diff --git a/src/calibre/gui2/convert/pdf_output.py b/src/calibre/gui2/convert/pdf_output.py index 1544d3f812..de0a34a508 100644 --- a/src/calibre/gui2/convert/pdf_output.py +++ b/src/calibre/gui2/convert/pdf_output.py @@ -16,9 +16,10 @@ class PluginWidget(Widget, Ui_Form): TITLE = _('PDF Output') HELP = _('Options specific to')+' PDF '+_('output') + COMMIT_NAME = 'pdf_output' def __init__(self, parent, get_option, get_help, db=None, book_id=None): - Widget.__init__(self, parent, 'pdf_output', ['paper_size', + Widget.__init__(self, parent, ['paper_size', 'orientation', 'preserve_cover_aspect_ratio']) self.db, self.book_id = db, book_id self.initialize_options(get_option, get_help, db, book_id) diff --git a/src/calibre/gui2/convert/rb_output.py b/src/calibre/gui2/convert/rb_output.py index 5fb214459f..5f8a961899 100644 --- a/src/calibre/gui2/convert/rb_output.py +++ b/src/calibre/gui2/convert/rb_output.py @@ -13,8 +13,9 @@ class PluginWidget(Widget, Ui_Form): TITLE = _('RB Output') HELP = _('Options specific to')+' RB '+_('output') + COMMIT_NAME = 'rb_output' def __init__(self, parent, get_option, get_help, db=None, book_id=None): - Widget.__init__(self, parent, 'rb_output', ['inline_toc']) + Widget.__init__(self, parent, ['inline_toc']) self.db, self.book_id = db, book_id self.initialize_options(get_option, get_help, db, book_id) diff --git a/src/calibre/gui2/convert/structure_detection.py b/src/calibre/gui2/convert/structure_detection.py index bf360f5315..cc1a16c617 100644 --- a/src/calibre/gui2/convert/structure_detection.py +++ b/src/calibre/gui2/convert/structure_detection.py @@ -18,9 +18,10 @@ class StructureDetectionWidget(Widget, Ui_Form): ICON = I('chapters.svg') HELP = _('Fine tune the detection of chapter headings and ' 'other document structure.') + COMMIT_NAME = 'structure_detection' def __init__(self, parent, get_option, get_help, db=None, book_id=None): - Widget.__init__(self, parent, 'structure_detection', + Widget.__init__(self, parent, ['chapter', 'chapter_mark', 'remove_first_image', 'insert_metadata', 'page_breaks_before', diff --git a/src/calibre/gui2/convert/toc.py b/src/calibre/gui2/convert/toc.py index dddce8d3ef..0908aba576 100644 --- a/src/calibre/gui2/convert/toc.py +++ b/src/calibre/gui2/convert/toc.py @@ -16,9 +16,10 @@ class TOCWidget(Widget, Ui_Form): TITLE = _('Table of\nContents') ICON = I('series.svg') HELP = _('Control the creation/conversion of the Table of Contents.') + COMMIT_NAME = 'toc' def __init__(self, parent, get_option, get_help, db=None, book_id=None): - Widget.__init__(self, parent, 'toc', + Widget.__init__(self, parent, ['level1_toc', 'level2_toc', 'level3_toc', 'toc_threshold', 'max_toc_links', 'no_chapters_in_toc', 'use_auto_toc', 'toc_filter', diff --git a/src/calibre/gui2/convert/txt_input.py b/src/calibre/gui2/convert/txt_input.py index f108bdd7d5..006c06dd00 100644 --- a/src/calibre/gui2/convert/txt_input.py +++ b/src/calibre/gui2/convert/txt_input.py @@ -11,9 +11,10 @@ class PluginWidget(Widget, Ui_Form): TITLE = _('TXT Input') HELP = _('Options specific to')+' TXT '+_('input') + COMMIT_NAME = 'txt_input' def __init__(self, parent, get_option, get_help, db=None, book_id=None): - Widget.__init__(self, parent, 'txt_input', + Widget.__init__(self, parent, ['single_line_paras', 'print_formatted_paras', 'markdown', 'markdown_disable_toc', 'preserve_spaces']) self.db, self.book_id = db, book_id diff --git a/src/calibre/gui2/convert/txt_output.py b/src/calibre/gui2/convert/txt_output.py index b79cd5779c..1accd08485 100644 --- a/src/calibre/gui2/convert/txt_output.py +++ b/src/calibre/gui2/convert/txt_output.py @@ -15,9 +15,10 @@ class PluginWidget(Widget, Ui_Form): TITLE = _('TXT Output') HELP = _('Options specific to')+' TXT '+_('output') + COMMIT_NAME = 'txt_output' def __init__(self, parent, get_option, get_help, db=None, book_id=None): - Widget.__init__(self, parent, 'txt_output', + Widget.__init__(self, parent, ['newline', 'max_line_length', 'force_max_line_length', 'inline_toc']) self.db, self.book_id = db, book_id diff --git a/src/calibre/gui2/dialogs/config/__init__.py b/src/calibre/gui2/dialogs/config/__init__.py index 3073a54774..8739b22012 100644 --- a/src/calibre/gui2/dialogs/config/__init__.py +++ b/src/calibre/gui2/dialogs/config/__init__.py @@ -29,6 +29,7 @@ from calibre.customize.ui import initialized_plugins, is_disabled, enable_plugin input_format_plugins, \ output_format_plugins, available_output_formats from calibre.utils.smtp import config as smtp_prefs +from calibre.gui2.convert import config_widget_for_input_plugin from calibre.gui2.convert.look_and_feel import LookAndFeelWidget from calibre.gui2.convert.page_setup import PageSetupWidget from calibre.gui2.convert.structure_detection import StructureDetectionWidget @@ -36,6 +37,7 @@ from calibre.ebooks.conversion.plumber import Plumber from calibre.utils.logging import Log from calibre.gui2.convert.toc import TOCWidget + class ConfigTabs(QTabWidget): def __init__(self, parent): @@ -58,15 +60,11 @@ class ConfigTabs(QTabWidget): self.widgets = [lf, ps, sd, toc] for plugin in input_format_plugins(): - name = plugin.name.lower().replace(' ', '_') - try: - input_widget = __import__('calibre.gui2.convert.'+name, - fromlist=[1]) + input_widget = config_widget_for_input_plugin(plugin) + if input_widget is not None: pw = input_widget.PluginWidget pw.ICON = I('forward.svg') self.widgets.append(widget_factory(pw)) - except ImportError: - continue for plugin in output_format_plugins(): name = plugin.name.lower().replace(' ', '_') diff --git a/src/calibre/gui2/tools.py b/src/calibre/gui2/tools.py index 57839d0ade..caef82ab81 100644 --- a/src/calibre/gui2/tools.py +++ b/src/calibre/gui2/tools.py @@ -22,6 +22,7 @@ from calibre.customize.conversion import OptionRecommendation from calibre.utils.config import prefs from calibre.ebooks.conversion.config import GuiRecommendations, \ load_defaults, load_specifics, save_specifics +from calibre.gui2.convert import bulk_defaults_for_input_format def convert_single_ebook(parent, db, book_ids, auto_conversion=False, out_format=None): changed = False @@ -148,7 +149,7 @@ class QueueBulk(QProgressDialog): temp_files = [] combined_recs = GuiRecommendations() - default_recs = load_defaults('%s_input' % input_format) + default_recs = bulk_defaults_for_input_format(input_format) for key in default_recs: combined_recs[key] = default_recs[key] if self.use_saved_single_settings: @@ -208,7 +209,6 @@ class QueueBulk(QProgressDialog): self.queue(self.jobs, self.changed, self.bad, *self.args) def fetch_scheduled_recipe(arg): - from calibre.ebooks.conversion.config import load_defaults fmt = prefs['output_format'].lower() pt = PersistentTemporaryFile(suffix='_recipe_out.%s'%fmt.lower()) pt.close() From 2884c0ca782db3fd35f763f5d26075da78ea89a5 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 14 Aug 2010 22:56:06 -0600 Subject: [PATCH 12/73] Fix regression in 0.7.13 that broke Comic Input when image output format was set to JPEG --- src/calibre/ebooks/comic/input.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/calibre/ebooks/comic/input.py b/src/calibre/ebooks/comic/input.py index 443a302999..23f5906a53 100755 --- a/src/calibre/ebooks/comic/input.py +++ b/src/calibre/ebooks/comic/input.py @@ -163,8 +163,12 @@ class PageProcessor(list): wand.quantize(self.opts.colors) dest = '%d_%d.%s'%(self.num, i, self.opts.output_format) dest = os.path.join(self.dest, dest) - wand.save(dest+'8') - os.rename(dest+'8', dest) + if dest.lower().endswith('.png'): + dest += '8' + wand.save(dest) + if dest.endswith('8'): + dest = dest[:-1] + os.rename(dest+'8', dest) self.append(dest) def render_pages(tasks, dest, opts, notification=lambda x, y: x): From 9805054fa1cf22f95d95d6d03315ca5df86111fe Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 14 Aug 2010 23:13:20 -0600 Subject: [PATCH 13/73] Remove entry from list of known libraries when quick switching, if no library found --- src/calibre/gui2/actions/choose_library.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/actions/choose_library.py b/src/calibre/gui2/actions/choose_library.py index bc22c93c27..713136029d 100644 --- a/src/calibre/gui2/actions/choose_library.py +++ b/src/calibre/gui2/actions/choose_library.py @@ -13,7 +13,7 @@ from PyQt4.Qt import QMenu from calibre import isbytestring from calibre.constants import filesystem_encoding from calibre.utils.config import prefs -from calibre.gui2 import gprefs +from calibre.gui2 import gprefs, warning_dialog from calibre.gui2.actions import InterfaceAction class LibraryUsageStats(object): @@ -34,6 +34,10 @@ class LibraryUsageStats(object): self.stats.pop(key) gprefs.set('library_usage_stats', self.stats) + def remove(self, location): + self.stats.pop(location, None) + self.write_stats() + def canonicalize_path(self, lpath): if isbytestring(lpath): lpath = lpath.decode(filesystem_encoding) @@ -123,6 +127,16 @@ class ChooseLibraryAction(InterfaceAction): def switch_requested(self, location): loc = location.replace('/', os.sep) + exists = self.gui.library_view.model().db.exists_at(loc) + if not exists: + warning_dialog(self.gui, _('No library found'), + _('No existing calibre library was found at %s.' + ' It will be removed from the list of known ' + ' libraries.')%loc, show=True) + self.stats.remove(location) + self.build_menus() + return + prefs['library_path'] = loc self.gui.library_moved(loc) From 5d9a2332e27d92911e1f8c60bc6d23b434c59ce1 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 14 Aug 2010 23:13:55 -0600 Subject: [PATCH 14/73] Updated translations template --- src/calibre/translations/calibre.pot | 1698 +++++++++++++------------- 1 file changed, 878 insertions(+), 820 deletions(-) diff --git a/src/calibre/translations/calibre.pot b/src/calibre/translations/calibre.pot index cf7aa6f652..5a92835907 100644 --- a/src/calibre/translations/calibre.pot +++ b/src/calibre/translations/calibre.pot @@ -5,8 +5,8 @@ msgid "" msgstr "" "Project-Id-Version: calibre 0.7.14\n" -"POT-Creation-Date: 2010-08-13 13:27+MDT\n" -"PO-Revision-Date: 2010-08-13 13:27+MDT\n" +"POT-Creation-Date: 2010-08-14 23:13+MDT\n" +"PO-Revision-Date: 2010-08-14 23:13+MDT\n" "Last-Translator: Automatically generated\n" "Language-Team: LANGUAGE\n" "MIME-Version: 1.0\n" @@ -30,7 +30,7 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/ebooks/chm/input.py:97 #: /home/kovid/work/calibre/src/calibre/ebooks/chm/input.py:100 #: /home/kovid/work/calibre/src/calibre/ebooks/chm/metadata.py:56 -#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:402 +#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:407 #: /home/kovid/work/calibre/src/calibre/ebooks/fb2/input.py:70 #: /home/kovid/work/calibre/src/calibre/ebooks/fb2/input.py:72 #: /home/kovid/work/calibre/src/calibre/ebooks/html/input.py:335 @@ -100,24 +100,24 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/ebooks/pdf/writer.py:98 #: /home/kovid/work/calibre/src/calibre/ebooks/rtf/input.py:234 #: /home/kovid/work/calibre/src/calibre/ebooks/rtf/input.py:236 -#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:287 -#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:294 -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:850 -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:853 +#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:293 +#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:300 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:281 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:284 #: /home/kovid/work/calibre/src/calibre/gui2/add.py:172 #: /home/kovid/work/calibre/src/calibre/gui2/add.py:179 -#: /home/kovid/work/calibre/src/calibre/gui2/convert/__init__.py:22 -#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:110 -#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:135 -#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:137 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:868 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:877 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1162 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1165 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/__init__.py:41 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:111 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:136 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:138 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:862 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:871 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1156 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1159 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf.py:47 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/fetch_metadata.py:120 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/fetch_metadata.py:155 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:512 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:513 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:173 #: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:362 #: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:382 @@ -168,6 +168,10 @@ msgstr "" msgid "Catalog generator" msgstr "" +#: /home/kovid/work/calibre/src/calibre/customize/__init__.py:359 +msgid "User Interface Action" +msgstr "" + #: /home/kovid/work/calibre/src/calibre/customize/builtins.py:16 msgid "Follow all local links in an HTML file and create a ZIP file containing all linked files. This plugin is run every time you add an HTML file to the library." msgstr "" @@ -230,15 +234,15 @@ msgstr "" msgid "Conversion Input" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/conversion.py:125 +#: /home/kovid/work/calibre/src/calibre/customize/conversion.py:129 msgid "Specify the character encoding of the input document. If set this option will override any encoding declared by the document itself. Particularly useful for documents that do not declare an encoding or that have erroneous encoding declarations." msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/conversion.py:237 +#: /home/kovid/work/calibre/src/calibre/customize/conversion.py:241 msgid "Conversion Output" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/conversion.py:251 +#: /home/kovid/work/calibre/src/calibre/customize/conversion.py:255 msgid "If specified, the output plugin will try to create output that is as human readable as possible. May not have any effect for some output plugins." msgstr "" @@ -375,11 +379,11 @@ msgstr "" msgid "No valid plugin found in " msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/ui.py:478 +#: /home/kovid/work/calibre/src/calibre/customize/ui.py:489 msgid "Initialization of plugin %s failed with traceback:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/ui.py:511 +#: /home/kovid/work/calibre/src/calibre/customize/ui.py:522 msgid "" " %prog options\n" "\n" @@ -387,27 +391,27 @@ msgid "" " " msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/ui.py:517 +#: /home/kovid/work/calibre/src/calibre/customize/ui.py:528 msgid "Add a plugin by specifying the path to the zip file containing it." msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/ui.py:519 +#: /home/kovid/work/calibre/src/calibre/customize/ui.py:530 msgid "Remove a custom plugin by name. Has no effect on builtin plugins" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/ui.py:521 +#: /home/kovid/work/calibre/src/calibre/customize/ui.py:532 msgid "Customize plugin. Specify name of plugin and customization string separated by a comma." msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/ui.py:523 +#: /home/kovid/work/calibre/src/calibre/customize/ui.py:534 msgid "List all installed plugins" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/ui.py:525 +#: /home/kovid/work/calibre/src/calibre/customize/ui.py:536 msgid "Enable the named plugin" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/ui.py:527 +#: /home/kovid/work/calibre/src/calibre/customize/ui.py:538 msgid "Disable the named plugin" msgstr "" @@ -661,7 +665,7 @@ msgid "Adding books to device metadata listing..." msgstr "" #: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:366 -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:458 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:249 msgid "Not Implemented" msgstr "" @@ -916,75 +920,75 @@ msgstr "" msgid "Set font delta" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:178 +#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:182 msgid "Rendered %s" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:181 +#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:185 msgid "Failed %s" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:235 +#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:239 msgid "" "Failed to process comic: \n" "\n" "%s" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:253 +#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:258 msgid "Number of colors for grayscale image conversion. Default: %default. Values of less than 256 may result in blurred text on your device if you are creating your comics in EPUB format." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:257 +#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:262 msgid "Disable normalize (improve contrast) color range for pictures. Default: False" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:260 +#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:265 msgid "Maintain picture aspect ratio. Default is to fill the screen." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:262 +#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:267 msgid "Disable sharpening." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:264 +#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:269 msgid "Disable trimming of comic pages. For some comics, trimming might remove content as well as borders." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:267 +#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:272 msgid "Don't split landscape images into two portrait images" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:269 +#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:274 msgid "Keep aspect ratio and scale image using screen height as image width for viewing in landscape mode." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:272 +#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:277 msgid "Used for right-to-left publications like manga. Causes landscape pages to be split into portrait pages from right to left." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:276 +#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:281 msgid "Enable Despeckle. Reduces speckle noise. May greatly increase processing time." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:279 +#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:284 msgid "Don't sort the files found in the comic alphabetically by name. Instead use the order they were added to the comic." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:283 +#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:288 msgid "The format that images in the created ebook are converted to. You can experiment to see which format gives you optimal size and look on your device." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:287 +#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:292 msgid "Apply no processing to the image" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:289 +#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:294 msgid "Do not convert the image to grayscale (black and white)" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:426 -#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:437 +#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:431 +#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:442 msgid "Page" msgstr "" @@ -2429,384 +2433,746 @@ msgstr "" msgid "Copy to Clipboard" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:397 +#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:403 msgid "Choose Files" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:67 -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:245 -msgid "Use library only" +#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:25 +msgid "A" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:68 -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:246 -msgid "User annotations generated from main library only" +#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:25 +msgid "Add books" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:75 -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:640 -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:706 -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:743 -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:764 -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:949 -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:1022 -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:1140 -msgid "No books selected" +#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:30 +msgid "Add books from a single directory" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:76 -msgid "No books selected to fetch annotations from" +#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:32 +msgid "Add books from directories, including sub-directories (One book per directory, assumes every ebook file is the same book in a different format)" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:101 -msgid "Merging user annotations into database" +#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:36 +msgid "Add books from directories, including sub directories (Multiple books per directory, assumes every ebook file is a different book)" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:129 -msgid "%s
Last Page Read: %d (%d%%)" +#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:40 +msgid "Add Empty book. (Book entry with no formats)" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:135 -msgid "%s
Last Page Read: Location %d (%d%%)" +#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:42 +msgid "Add from ISBN" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:154 -msgid "Location %d • %s
%s
" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:163 -msgid "Page %d • %s
" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:168 -msgid "Location %d • %s
" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:291 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:81 msgid "How many empty books?" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:292 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:82 msgid "How many empty books should be added?" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:350 -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:407 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:140 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:198 msgid "Uploading books to device." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:367 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:164 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:157 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:165 msgid "Books" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:368 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:158 msgid "EPUB Books" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:369 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:159 msgid "LRF Books" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:370 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:160 msgid "HTML Books" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:371 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:161 msgid "LIT Books" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:372 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:162 msgid "MOBI Books" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:373 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:163 msgid "Topaz books" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:374 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:164 msgid "Text books" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:375 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:165 msgid "PDF Books" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:376 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:166 msgid "Comics" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:377 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:167 msgid "Archives" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:381 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:171 msgid "Supported books" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:416 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:207 msgid "Merged some books" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:417 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:208 msgid "Some duplicates were found and merged into the following existing books:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:426 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:217 msgid "Failed to read metadata" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:427 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:218 msgid "Failed to read metadata from the following" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:446 -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:465 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:237 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:256 msgid "Add to library" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:446 -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:490 -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:1284 -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:1309 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:237 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:55 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:94 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:119 msgid "No book selected" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:459 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:250 msgid "The following books are virtual and cannot be added to the calibre library:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:465 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:256 msgid "No book files found" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:487 -msgid "Cannot delete" +#: /home/kovid/work/calibre/src/calibre/gui2/actions/add_to_library.py:13 +msgid "Add books to library" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:500 -msgid "Choose formats to be deleted" +#: /home/kovid/work/calibre/src/calibre/gui2/actions/annotate.py:20 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:499 +msgid "Fetch annotations (experimental)" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:518 -msgid "Choose formats not to be deleted" +#: /home/kovid/work/calibre/src/calibre/gui2/actions/annotate.py:55 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/annotate.py:235 +msgid "Use library only" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:538 -msgid "Cannot delete books" +#: /home/kovid/work/calibre/src/calibre/gui2/actions/annotate.py:56 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/annotate.py:236 +msgid "User annotations generated from main library only" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:539 -msgid "No device is connected" +#: /home/kovid/work/calibre/src/calibre/gui2/actions/annotate.py:63 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/catalog.py:30 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/convert.py:85 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:73 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:138 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:174 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:195 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:91 +msgid "No books selected" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:549 -msgid "Main memory" +#: /home/kovid/work/calibre/src/calibre/gui2/actions/annotate.py:64 +msgid "No books selected to fetch annotations from" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:550 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:436 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:445 -msgid "Storage Card A" +#: /home/kovid/work/calibre/src/calibre/gui2/actions/annotate.py:89 +msgid "Merging user annotations into database" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:551 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:438 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:447 -msgid "Storage Card B" +#: /home/kovid/work/calibre/src/calibre/gui2/actions/annotate.py:117 +msgid "%s
Last Page Read: %d (%d%%)" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:556 -msgid "No books to delete" +#: /home/kovid/work/calibre/src/calibre/gui2/actions/annotate.py:123 +msgid "%s
Last Page Read: Location %d (%d%%)" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:557 -msgid "None of the selected books are on the device" +#: /home/kovid/work/calibre/src/calibre/gui2/actions/annotate.py:142 +msgid "Location %d • %s
%s
" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:574 -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:629 -msgid "Deleting books from device." +#: /home/kovid/work/calibre/src/calibre/gui2/actions/annotate.py:151 +msgid "Page %d • %s
" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:595 -msgid "The selected books will be permanently deleted and the files removed from your computer. Are you sure?" +#: /home/kovid/work/calibre/src/calibre/gui2/actions/annotate.py:156 +msgid "Location %d • %s
" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:614 -msgid "The selected books will be permanently deleted from your device. Are you sure?" +#: /home/kovid/work/calibre/src/calibre/gui2/actions/catalog.py:20 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/convert.py:32 +msgid "Create catalog of books in your calibre library" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:639 -msgid "Cannot download metadata" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:662 -msgid "social metadata" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:664 -msgid "covers" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:664 -msgid "metadata" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:666 -msgid "Downloading %s for %d book(s)" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:690 -msgid "Failed to download some metadata" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:691 -msgid "Failed to download metadata for the following:" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:694 -msgid "Failed to download metadata:" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:695 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:608 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:560 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:989 -#: /home/kovid/work/calibre/src/calibre/utils/ipc/job.py:53 -msgid "Error" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:705 -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:742 -msgid "Cannot edit metadata" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:763 -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:766 -msgid "Cannot merge books" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:767 -msgid "At least two books must be selected for merging" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:771 -msgid "All book formats and metadata from the selected books will be added to the first selected book.

The second and subsequently selected books will not be deleted or changed.

Please confirm you want to proceed." -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:782 -msgid "All book formats and metadata from the selected books will be merged into the first selected book.

After merger the second and subsequently selected books will be deleted.

All book formats of the first selected book will be kept and any duplicate formats in the second and subsequently selected books will be permanently deleted from your computer.

Are you sure you want to proceed?" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:794 -msgid "You are about to merge more than 5 books. Are you sure you want to proceed?" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:948 -msgid "Cannot save to disk" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:951 -msgid "Choose destination directory" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:957 -#: /home/kovid/work/calibre/src/calibre/gui2/library/views.py:566 -msgid "Not allowed" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:958 -msgid "You are trying to save files into the calibre library. This can cause corruption of your library. Save to disk is meant to export files from your calibre library elsewhere." -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:992 -msgid "Error while saving" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:993 -msgid "There was an error while saving." -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:1000 -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:1001 -msgid "Could not save some books" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:1002 -msgid "Click the show details button to see which ones." -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:1023 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/catalog.py:31 msgid "No books selected to generate catalog for" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:1040 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/catalog.py:48 msgid "Generating %s catalog..." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:1045 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/catalog.py:53 #: /home/kovid/work/calibre/src/calibre/gui2/add.py:264 msgid "No books found" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:1046 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/catalog.py:54 msgid "" "No books to catalog\n" "Check exclude tags" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:1056 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/catalog.py:64 msgid "Catalog generated." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:1059 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/catalog.py:67 msgid "Export Catalog Directory" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:1060 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/catalog.py:68 msgid "Select destination for %s.%s" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:1076 -msgid "Fetching news from " +#: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:73 +#: /home/kovid/work/calibre/src/calibre/library/server/opds.py:108 +msgid "%d books" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:1090 -msgid " fetched." +#: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:74 +msgid "Choose calibre library to work with" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:1139 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:81 +msgid "Switch to library..." +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:88 +msgid "Quick switch" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:132 +msgid "No library found" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:133 +msgid "No existing calibre library was found at %s. It will be removed from the list of known libraries." +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/convert.py:22 +msgid "C" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/convert.py:22 +msgid "Convert books" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/convert.py:26 +msgid "Convert individually" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/convert.py:28 +msgid "Bulk convert" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/convert.py:84 msgid "Cannot convert" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:1168 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/convert.py:113 msgid "Starting conversion of %d book(s)" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:1284 -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:1345 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:18 +msgid "Del" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:18 +msgid "Remove books" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:23 +msgid "Remove selected books" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:25 +msgid "Remove files of a specific format from selected books.." +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:28 +msgid "Remove all formats from selected books, except..." +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:31 +msgid "Remove covers from selected books" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:34 +msgid "Remove matching books from device" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:52 +msgid "Cannot delete" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:65 +msgid "Choose formats to be deleted" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:83 +msgid "Choose formats not to be deleted" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:103 +msgid "Cannot delete books" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:104 +msgid "No device is connected" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:114 +msgid "Main memory" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:115 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:436 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:445 +msgid "Storage Card A" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:116 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:438 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:447 +msgid "Storage Card B" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:121 +msgid "No books to delete" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:122 +msgid "None of the selected books are on the device" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:139 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:194 +msgid "Deleting books from device." +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:160 +msgid "The selected books will be permanently deleted and the files removed from your computer. Are you sure?" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:179 +msgid "The selected books will be permanently deleted from your device. Are you sure?" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/device.py:25 +msgid "Connect to folder" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/device.py:30 +msgid "Connect to iTunes" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/device.py:39 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/device.py:47 +msgid "Start Content Server" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/device.py:49 +msgid "Stop Content Server" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/device.py:60 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/device.py:66 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/device.py:68 +msgid "Email to" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/device.py:68 +msgid " and delete from library" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/device.py:80 +msgid "Setup email based sharing of books" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/device.py:97 +msgid "D" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/device.py:97 +msgid "Send to device" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/device.py:114 +msgid "Connect/share" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_collections.py:13 +msgid "Manage collections" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:23 +msgid "E" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:23 +msgid "Edit metadata" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:26 +msgid "Merge book records" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:27 +msgid "M" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:29 +msgid "Edit metadata individually" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:32 +msgid "Edit metadata in bulk" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:35 +msgid "Download metadata and covers" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:38 +msgid "Download only metadata" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:40 +msgid "Download only covers" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:43 +msgid "Download only social metadata" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:49 +msgid "Merge into first selected book - delete others" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:52 +msgid "Merge into first selected book - keep others" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:72 +msgid "Cannot download metadata" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:95 +msgid "social metadata" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:97 +msgid "covers" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:97 +msgid "metadata" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:99 +msgid "Downloading %s for %d book(s)" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:122 +msgid "Failed to download some metadata" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:123 +msgid "Failed to download metadata for the following:" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:126 +msgid "Failed to download metadata:" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:127 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:608 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:558 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:987 +#: /home/kovid/work/calibre/src/calibre/utils/ipc/job.py:54 +msgid "Error" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:137 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:173 +msgid "Cannot edit metadata" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:194 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:197 +msgid "Cannot merge books" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:198 +msgid "At least two books must be selected for merging" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:202 +msgid "All book formats and metadata from the selected books will be added to the first selected book.

The second and subsequently selected books will not be deleted or changed.

Please confirm you want to proceed." +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:213 +msgid "All book formats and metadata from the selected books will be merged into the first selected book.

After merger the second and subsequently selected books will be deleted.

All book formats of the first selected book will be kept and any duplicate formats in the second and subsequently selected books will be permanently deleted from your computer.

Are you sure you want to proceed?" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:225 +msgid "You are about to merge more than 5 books. Are you sure you want to proceed?" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/fetch_news.py:18 +msgid "F" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/fetch_news.py:18 +msgid "Fetch news" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/fetch_news.py:48 +msgid "Fetching news from " +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/fetch_news.py:62 +msgid " fetched." +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/help.py:16 +msgid "Browse the calibre User Manual" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/help.py:16 +msgid "F1" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/help.py:16 +msgid "Help" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/open.py:14 +msgid "Open containing folder" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/open.py:15 +msgid "O" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/preferences.py:17 +msgid "Ctrl+P" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/preferences.py:17 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/preferences.py:21 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:584 +#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main_ui.py:201 +msgid "Preferences" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/preferences.py:22 +msgid "Run welcome wizard" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/preferences.py:32 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/preferences.py:37 +msgid "Cannot configure" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/preferences.py:33 +msgid "Cannot configure while there are running jobs." +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/preferences.py:38 +msgid "Cannot configure before calibre is restarted." +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/restart.py:14 +msgid "&Restart" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/restart.py:14 +msgid "Ctrl+R" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:24 +msgid "Save single format to disk..." +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:40 +msgid "S" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:40 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:45 +msgid "Save to disk" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:47 +msgid "Save to disk in a single directory" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:49 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:68 +msgid "Save only %s format to disk" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:53 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:71 +msgid "Save only %s format to disk in a single directory" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:90 +msgid "Cannot save to disk" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:93 +msgid "Choose destination directory" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:100 +#: /home/kovid/work/calibre/src/calibre/gui2/library/views.py:540 +msgid "Not allowed" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:101 +msgid "You are trying to save files into the calibre library. This can cause corruption of your library. Save to disk is meant to export files from your calibre library elsewhere." +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:135 +msgid "Error while saving" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:136 +msgid "There was an error while saving." +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:143 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:144 +msgid "Could not save some books" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:145 +msgid "Click the show details button to see which ones." +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/show_book_details.py:16 +msgid "Show book details" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/show_book_details.py:17 +msgid "I" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/show_book_details.py:24 +msgid "No detailed info available" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/show_book_details.py:25 +msgid "No detailed information is available for books on the device." +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/similar_books.py:17 +msgid "Similar books..." +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/similar_books.py:22 +msgid "Alt+A" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/similar_books.py:22 +msgid "Books by same author" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/similar_books.py:23 +msgid "Alt+S" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/similar_books.py:23 +msgid "Books in this series" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/similar_books.py:24 +msgid "Alt+P" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/similar_books.py:24 +msgid "Books by this publisher" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/similar_books.py:25 +msgid "Alt+T" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/similar_books.py:25 +msgid "Books with the same tags" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:24 +msgid "V" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:24 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:31 +msgid "View" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:32 +msgid "View specific format" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:94 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:155 msgid "Cannot view" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:1290 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:100 #: /home/kovid/work/calibre/src/calibre/gui2/convert/regex_builder.py:77 msgid "Choose the format to view" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:1298 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:108 msgid "Multiple Books Selected" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:1299 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:109 msgid "You are attempting to open %d books. Opening too many books at once can be slow and have a negative effect on the responsiveness of your computer. Once started the process cannot be stopped until complete. Do you wish to continue?" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:1308 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:118 msgid "Cannot open folder" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/__init__.py:1346 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:156 msgid "%s has no available formats." msgstr "" @@ -2831,7 +3197,7 @@ msgid "The specified directory could not be processed." msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/add.py:263 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:810 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:804 msgid "No books" msgstr "" @@ -3014,7 +3380,7 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/book_info.py:118 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/book_info.py:119 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/book_info.py:122 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:230 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:228 #: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:312 #: /home/kovid/work/calibre/src/calibre/library/field_metadata.py:100 msgid "Formats" @@ -3345,16 +3711,16 @@ msgstr "" msgid "Debug the conversion process." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/debug.py:38 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/debug.py:39 #: /home/kovid/work/calibre/src/calibre/gui2/convert/debug_ui.py:51 msgid "Choose debug folder" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/debug.py:57 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/debug.py:58 msgid "Invalid debug directory" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/debug.py:58 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/debug.py:59 msgid "Failed to create debug directory" msgstr "" @@ -3481,15 +3847,15 @@ msgstr "" msgid "Control the look and feel of the output" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel.py:30 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel.py:31 msgid "Original" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel.py:31 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel.py:32 msgid "Left align" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel.py:32 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel.py:33 msgid "Justify text" msgstr "" @@ -3615,34 +3981,34 @@ msgstr "" msgid "Set the metadata. The output file will contain as much of this metadata as possible." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:164 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:111 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:165 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:112 msgid "Choose cover for " msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:171 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:118 -msgid "Cannot read" -msgstr "" - #: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:172 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:119 +msgid "Cannot read" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:173 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:120 msgid "You do not have permission to read the file: " msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:180 -#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:187 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:127 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:181 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:188 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:128 msgid "Error reading file" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:181 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:128 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:182 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:129 msgid "

There was an error reading from file:
" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:188 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:136 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:189 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:137 msgid " is not a valid picture" msgstr "" @@ -3918,39 +4284,39 @@ msgstr "" msgid "Fine tune the detection of chapter headings and other document structure." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:34 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:35 msgid "Detect chapters at (XPath expression):" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:35 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:36 msgid "Insert page breaks before (XPath expression):" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:37 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:38 msgid "Header regular expression:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:40 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:41 msgid "Footer regular expression:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:56 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:57 #: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:76 msgid "Invalid regular expression" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:57 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:58 #: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:77 msgid "Invalid regular expression: %s" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:62 -#: /home/kovid/work/calibre/src/calibre/gui2/convert/toc.py:38 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:63 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/toc.py:39 msgid "Invalid XPath" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:63 -#: /home/kovid/work/calibre/src/calibre/gui2/convert/toc.py:39 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:64 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/toc.py:40 msgid "The XPath expression %s is invalid." msgstr "" @@ -3994,15 +4360,15 @@ msgstr "" msgid "Control the creation/conversion of the Table of Contents." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/toc.py:29 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/toc.py:30 msgid "Level &1 TOC (XPath expression):" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/toc.py:30 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/toc.py:31 msgid "Level &2 TOC (XPath expression):" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/toc.py:31 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/toc.py:32 msgid "Level &3 TOC (XPath expression):" msgstr "" @@ -4213,7 +4579,7 @@ msgid "tags to remove" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/device.py:49 -#: /home/kovid/work/calibre/src/calibre/utils/ipc/job.py:135 +#: /home/kovid/work/calibre/src/calibre/utils/ipc/job.py:136 msgid "No details available." msgstr "" @@ -4290,153 +4656,149 @@ msgstr "" msgid "Eject device" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:499 -msgid "Fetch annotations (experimental)" -msgstr "" - #: /home/kovid/work/calibre/src/calibre/gui2/device.py:609 msgid "Error communicating with device" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:631 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:629 msgid "Select folder to open as device" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:675 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:671 msgid "Failed" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:681 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:677 msgid "Error talking to device" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:682 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:678 msgid "There was a temporary error talking to the device. Please unplug and reconnect the device and or reboot." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:724 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:717 msgid "Device: " msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:726 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:719 msgid " detected." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:811 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:805 msgid "selected to send" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:816 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:810 msgid "Choose format to send to device" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:825 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:819 msgid "No device" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:826 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:820 msgid "Cannot send: No device is connected" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:829 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:833 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:823 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:827 msgid "No card" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:830 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:834 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:824 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:828 msgid "Cannot send: Device has no storage card" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:875 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:869 msgid "E-book:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:878 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:872 msgid "Attached, you will find the e-book" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:879 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:181 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:873 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:179 msgid "by" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:880 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:874 msgid "in the %s format." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:893 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:887 msgid "Sending email to" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:923 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:931 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1025 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1087 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1206 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1214 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:917 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:925 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1019 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1081 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1200 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1208 msgid "No suitable formats" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:924 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:918 msgid "Auto convert the following books before sending via email?" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:932 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:926 msgid "Could not email the following books as no suitable formats were found:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:950 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:944 msgid "Failed to email books" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:951 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:945 msgid "Failed to email the following books:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:955 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:949 msgid "Sent by email:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:984 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:978 msgid "News:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:985 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:979 msgid "Attached is the" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:996 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:990 msgid "Sent news to" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1026 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1088 -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1207 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1020 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1082 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1201 msgid "Auto convert the following books before uploading to the device?" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1056 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1050 msgid "Sending catalogs to device." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1120 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1114 msgid "Sending news to device." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1173 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1167 msgid "Sending books to device." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1215 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1209 msgid "Could not upload the following books to the device, as no suitable formats were found. Convert the book(s) to a format supported by your device first." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1277 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1271 msgid "No space on device" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1278 +#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1272 msgid "

Cannot upload books to device there is no more free space available " msgstr "" @@ -4553,6 +4915,14 @@ msgstr "" msgid "No location selected" msgstr "" +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/choose_library.py:84 +msgid "Bad location" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/choose_library.py:85 +msgid "%s is not an existing folder" +msgstr "" + #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/choose_library_ui.py:66 msgid "Choose your calibre library" msgstr "" @@ -4601,247 +4971,247 @@ msgstr "" msgid "Edit Comments" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:174 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:172 msgid "%(plugin_type)s %(plugins)s" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:175 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:173 msgid "plugins" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:184 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:182 msgid "" "\n" "Customization: " msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:199 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:197 msgid "General" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:200 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:198 msgid "Interface" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:201 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:199 msgid "Conversion" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:202 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:200 msgid "" "Email\n" "Delivery" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:203 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:201 msgid "Add/Save" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:204 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:202 msgid "Advanced" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:205 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:203 msgid "" "Content\n" "Server" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:206 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:204 msgid "Plugins" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:230 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:228 msgid "Auto send" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:230 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:228 msgid "Email" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:235 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:233 msgid "Formats to email. The first matching format will be sent." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:236 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:234 msgid "If checked, downloaded news will be automatically mailed
to this email address (provided it is in one of the listed formats)." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:310 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:308 msgid "new email address" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:492 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:490 msgid "Wide" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:493 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:491 msgid "Narrow" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:502 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:500 msgid "Medium" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:502 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:500 msgid "Small" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:503 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:501 msgid "Large" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:509 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:507 msgid "Always" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:509 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:507 msgid "Automatic" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:510 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:508 msgid "Never" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:534 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:532 msgid "Done" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:535 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:533 msgid "Confirmation dialogs have all been reset" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:540 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:538 msgid "System port selected" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:541 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:539 msgid "The value %d you have chosen for the content server port is a system port. Your operating system may not allow the server to run on this port. To be safe choose a port number larger than 1024." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:561 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:559 msgid "Failed to install command line tools." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:564 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:562 msgid "Command line tools installed" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:565 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:563 msgid "Command line tools installed in" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:566 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:564 msgid "If you move calibre.app, you have to re-install the command line tools." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:617 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:615 msgid "No valid plugin path" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:618 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:616 msgid "%s is not a valid plugin path" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:621 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:619 msgid "Choose plugin" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:633 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:631 msgid "Plugin cannot be disabled" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:634 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:632 msgid "The plugin: %s cannot be disabled" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:643 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:641 msgid "Plugin not customizable" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:644 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:642 msgid "Plugin: %s does not need customization" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:652 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:650 msgid "Customize" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:690 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:688 msgid "Cannot remove builtin plugin" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:691 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:689 msgid " cannot be removed. It is a builtin plugin. Try disabling it instead." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:706 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:704 msgid "Invalid tweaks" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:707 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:705 msgid "The tweaks you entered are invalid, try resetting the tweaks to default and changing them one by one until you find the invalid setting." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:737 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:735 msgid "You must select a column to delete it" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:742 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:740 msgid "The selected column is not a custom column" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:743 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:741 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/confirm_delete_ui.py:48 msgid "Are you sure?" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:744 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:742 msgid "Do you really want to delete column %s and all its data?" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:811 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:809 msgid "Error log:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:818 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:816 msgid "Access log:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:846 -#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:318 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:844 +#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:313 msgid "Failed to start content server" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:871 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:869 msgid "Invalid size" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:872 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:870 msgid "The size %s is invalid. must be of the form widthxheight" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:935 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:933 msgid "Must restart" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:936 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:934 msgid "The changes you made require that Calibre be restarted. Please restart as soon as practical." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:970 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:968 msgid "Checking database integrity" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:990 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:988 msgid "Failed to check database integrity" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:995 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:993 msgid "Some inconsistencies found" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:996 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:994 msgid "The following books had formats listed in the database that are not actually available. The entries for the formats have been removed. You should check them manually. This can happen if you manipulate the files in the library folder directly." msgstr "" @@ -4956,13 +5326,6 @@ msgstr "" msgid "Sending to &device" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:584 -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:474 -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:618 -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main_ui.py:201 -msgid "Preferences" -msgstr "" - #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:585 msgid "Show notification when &new version is available" msgstr "" @@ -5774,135 +6137,135 @@ msgstr "" msgid "Last modified: %s" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:135 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:136 msgid "Not a valid picture" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:150 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:151 msgid "Specify title and author" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:151 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:152 msgid "You must specify a title and author before generating a cover" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:163 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:164 msgid "Choose formats for " msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:194 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:195 msgid "No permission" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:195 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:196 msgid "You do not have permission to read the following files:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:222 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:223 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:224 msgid "No format selected" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:234 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:235 msgid "Could not read metadata" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:235 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:236 msgid "Could not read metadata from %s format" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:283 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:289 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:284 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:290 msgid "Could not read cover" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:284 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:285 msgid "Could not read cover from %s format" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:290 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:291 msgid "The cover in the %s format is invalid" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:327 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:328 msgid "Abort the editing of all remaining books" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:465 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:470 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:466 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:471 msgid "This ISBN number is valid" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:473 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:474 msgid "This ISBN number is invalid" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:552 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:553 msgid "Cannot use tag editor" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:553 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:554 msgid "The tags editor cannot be used if you have modified the tags" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:573 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:574 msgid "Downloading cover..." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:585 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:590 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:596 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:601 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:586 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:591 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:597 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:602 msgid "Cannot fetch cover" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:586 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:597 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:602 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:587 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:598 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:603 msgid "Could not fetch cover.
" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:587 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:588 msgid "The download timed out." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:591 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:592 msgid "Could not find cover for this book. Try specifying the ISBN first." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:603 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:604 msgid "For the error message from each cover source, click Show details below." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:610 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:611 msgid "Bad cover" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:611 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:612 msgid "The cover is not a valid picture" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:644 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:645 msgid "There were errors" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:645 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:646 msgid "There were errors downloading social metadata" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:674 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:675 msgid "Cannot fetch metadata" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:675 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:676 msgid "You must specify at least one of ISBN, Title, Authors or Publisher" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:750 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:751 msgid "Permission denied" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:751 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:752 msgid "Could not open %s. Is it being used by another program?" msgstr "" @@ -5994,7 +6357,7 @@ msgstr "" msgid "Password needed" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/progress.py:52 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/progress.py:54 msgid "Aborting..." msgstr "" @@ -6684,62 +7047,48 @@ msgstr "" msgid "Regular expression (?P)" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/init.py:33 -msgid "Similar books..." -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/init.py:66 -msgid "Add books to library" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/init.py:68 -#: /home/kovid/work/calibre/src/calibre/gui2/init.py:76 -#: /home/kovid/work/calibre/src/calibre/gui2/init.py:84 -msgid "Manage collections" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/init.py:165 +#: /home/kovid/work/calibre/src/calibre/gui2/init.py:104 msgid "Cover Browser" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/init.py:170 +#: /home/kovid/work/calibre/src/calibre/gui2/init.py:109 msgid "Shift+Alt+B" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/init.py:184 +#: /home/kovid/work/calibre/src/calibre/gui2/init.py:123 msgid "Tag Browser" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/init.py:186 +#: /home/kovid/work/calibre/src/calibre/gui2/init.py:125 msgid "Shift+Alt+T" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/init.py:206 +#: /home/kovid/work/calibre/src/calibre/gui2/init.py:145 msgid "version" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/init.py:207 +#: /home/kovid/work/calibre/src/calibre/gui2/init.py:146 msgid "created by Kovid Goyal" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/init.py:225 +#: /home/kovid/work/calibre/src/calibre/gui2/init.py:164 msgid "Connected " msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/init.py:234 +#: /home/kovid/work/calibre/src/calibre/gui2/init.py:173 msgid "Update found" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/init.py:278 -#: /home/kovid/work/calibre/src/calibre/gui2/init.py:288 +#: /home/kovid/work/calibre/src/calibre/gui2/init.py:217 +#: /home/kovid/work/calibre/src/calibre/gui2/init.py:227 msgid "Book Details" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/init.py:280 +#: /home/kovid/work/calibre/src/calibre/gui2/init.py:219 msgid "Alt+D" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/init.py:290 +#: /home/kovid/work/calibre/src/calibre/gui2/init.py:229 msgid "Shift+Alt+D" msgstr "" @@ -6773,372 +7122,106 @@ msgstr "" msgid "There are %d waiting jobs:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:216 -#: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:219 +#: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:217 +#: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:220 msgid "Cannot kill job" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:217 +#: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:218 msgid "Cannot kill jobs that communicate with the device" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:220 +#: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:221 msgid "Job has already run" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:248 +#: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:249 msgid "Unavailable" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:280 +#: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:281 msgid "Jobs:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:298 +#: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:299 msgid "Click to see list of active jobs." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:358 +#: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:359 msgid " - Jobs" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:33 -msgid "Save single format to disk..." -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:76 +#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:66 msgid "Eject this device" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:85 +#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:75 #: /home/kovid/work/calibre/src/calibre/library/server/opds.py:192 msgid "Library" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:86 +#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:76 msgid "Show books in calibre library" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:87 +#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:77 msgid "Reader" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:88 +#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:78 msgid "Show books in the main memory of the device" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:89 +#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:79 #: /home/kovid/work/calibre/src/calibre/library/database2.py:593 msgid "Card A" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:90 +#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:80 msgid "Show books in storage card A" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:91 +#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:81 #: /home/kovid/work/calibre/src/calibre/library/database2.py:595 msgid "Card B" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:92 +#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:82 msgid "Show books in storage card B" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:131 +#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:121 msgid "available" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:162 +#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:152 msgid "Books display will be restricted to those matching the selected saved search" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:175 +#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:165 msgid "Advanced search" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:184 +#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:174 msgid "

Search the list of books by title, author, publisher, tags, comments, etc.

Words separated by spaces are ANDed" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:191 +#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:181 msgid "Reset Quick Search" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:203 +#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:193 msgid "Copy current search text (instead of search name)" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:209 +#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:199 msgid "Save current search under the name shown in the box" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:215 +#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:205 msgid "Delete current saved search" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:302 -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:467 -#: /home/kovid/work/calibre/src/calibre/library/server/opds.py:108 -msgid "%d books" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:305 -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:468 -msgid "Choose calibre library to work with" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:336 -msgid "Connect to folder" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:341 -msgid "Connect to iTunes" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:348 -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:356 -msgid "Start Content Server" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:358 -msgid "Stop Content Server" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:369 -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:375 -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:377 -msgid "Email to" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:377 -msgid " and delete from library" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:389 -msgid "Setup email based sharing of books" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:462 -msgid "A" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:462 -msgid "Add books" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:463 -msgid "E" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:463 -msgid "Edit metadata" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:464 -msgid "C" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:464 -msgid "Convert books" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:465 -msgid "V" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:465 -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:570 -msgid "View" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:466 -msgid "Send to device" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:469 -msgid "F" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:469 -msgid "Fetch news" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:470 -msgid "S" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:470 -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:553 -msgid "Save to disk" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:471 -msgid "Connect/share" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:472 -msgid "Del" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:472 -msgid "Remove books" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:473 -msgid "Browse the calibre User Manual" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:473 -msgid "F1" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:473 -msgid "Help" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:474 -msgid "Ctrl+P" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:476 -msgid "M" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:476 -msgid "Merge book records" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:477 -msgid "Open containing folder" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:479 -msgid "Show book details" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:481 -msgid "Books by same author" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:483 -msgid "Books in this series" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:485 -msgid "Books by this publisher" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:487 -msgid "Books with the same tags" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:501 -msgid "Edit metadata individually" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:504 -msgid "Edit metadata in bulk" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:507 -msgid "Download metadata and covers" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:510 -msgid "Download only metadata" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:512 -msgid "Download only covers" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:515 -msgid "Download only social metadata" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:521 -msgid "Merge into first selected book - delete others" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:524 -msgid "Merge into first selected book - keep others" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:532 -msgid "Add books from a single directory" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:534 -msgid "Add books from directories, including sub-directories (One book per directory, assumes every ebook file is the same book in a different format)" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:538 -msgid "Add books from directories, including sub directories (Multiple books per directory, assumes every ebook file is a different book)" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:542 -msgid "Add Empty book. (Book entry with no formats)" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:544 -msgid "Add from ISBN" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:555 -msgid "Save to disk in a single directory" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:557 -#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:385 -msgid "Save only %s format to disk" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:561 -#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:388 -msgid "Save only %s format to disk in a single directory" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:571 -msgid "View specific format" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:577 -msgid "Remove selected books" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:579 -msgid "Remove files of a specific format from selected books.." -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:582 -msgid "Remove all formats from selected books, except..." -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:585 -msgid "Remove covers from selected books" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:588 -msgid "Remove matching books from device" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:605 -msgid "Convert individually" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:607 -msgid "Bulk convert" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:611 -msgid "Create catalog of books in your calibre library" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:619 -msgid "Run welcome wizard" -msgstr "" - #: /home/kovid/work/calibre/src/calibre/gui2/library/delegates.py:284 msgid "N" msgstr "" @@ -7226,7 +7309,7 @@ msgstr "" msgid "Restore default layout" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/library/views.py:567 +#: /home/kovid/work/calibre/src/calibre/gui2/library/views.py:541 msgid "Dropping onto a device is not supported. First add the book to the calibre library." msgstr "" @@ -7622,26 +7705,26 @@ msgstr "" msgid "Add your own categories to the Tag Browser" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/tools.py:63 -#: /home/kovid/work/calibre/src/calibre/gui2/tools.py:182 +#: /home/kovid/work/calibre/src/calibre/gui2/tools.py:64 +#: /home/kovid/work/calibre/src/calibre/gui2/tools.py:183 msgid "Convert book %d of %d (%s)" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/tools.py:90 -#: /home/kovid/work/calibre/src/calibre/gui2/tools.py:202 -msgid "Could not convert some books" -msgstr "" - #: /home/kovid/work/calibre/src/calibre/gui2/tools.py:91 #: /home/kovid/work/calibre/src/calibre/gui2/tools.py:203 +msgid "Could not convert some books" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/tools.py:92 +#: /home/kovid/work/calibre/src/calibre/gui2/tools.py:204 msgid "Could not convert %d of %d books, because no suitable source format was found." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/tools.py:120 +#: /home/kovid/work/calibre/src/calibre/gui2/tools.py:121 msgid "Queueing books for bulk conversion" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/tools.py:181 +#: /home/kovid/work/calibre/src/calibre/gui2/tools.py:182 msgid "Queueing " msgstr "" @@ -7657,84 +7740,59 @@ msgstr "" msgid "The following books have already been converted to %s format. Do you wish to reconvert them?" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:160 +#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:165 msgid "&Restore" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:162 +#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:167 msgid "&Donate to support calibre" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:166 +#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:171 msgid "&Eject connected device" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:169 -msgid "&Restart" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:217 +#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:212 msgid "Calibre Quick Start Guide" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:362 -#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:367 -msgid "Cannot configure" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:363 -msgid "Cannot configure while there are running jobs." -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:368 -msgid "Cannot configure before calibre is restarted." -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:415 -msgid "No detailed info available" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:416 -msgid "No detailed information is available for books on the device." -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:468 -#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:496 +#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:406 +#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:434 msgid "Conversion Error" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:469 +#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:407 msgid "

Could not convert: %s

It is a DRMed book. You must first remove the DRM using third party tools." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:482 +#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:420 msgid "Recipe Disabled" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:497 +#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:435 msgid "Failed" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:537 +#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:471 msgid "is the result of the efforts of many volunteers from all over the world. If you find it useful, please consider donating to support its development. Your donation helps keep calibre development going." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:563 +#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:497 msgid "There are active jobs. Are you sure you want to quit?" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:566 +#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:500 msgid "" " is communicating with the device!
\n" " Quitting may cause corruption on the device.
\n" " Are you sure you want to quit?" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:570 +#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:504 msgid "WARNING: Active jobs" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:625 +#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:559 msgid "will keep running in the system tray. To close it, choose Quit in the context menu of the system tray." msgstr "" @@ -9284,15 +9342,15 @@ msgstr "" msgid "Waiting..." msgstr "" -#: /home/kovid/work/calibre/src/calibre/utils/ipc/job.py:51 +#: /home/kovid/work/calibre/src/calibre/utils/ipc/job.py:52 msgid "Stopped" msgstr "" -#: /home/kovid/work/calibre/src/calibre/utils/ipc/job.py:53 +#: /home/kovid/work/calibre/src/calibre/utils/ipc/job.py:54 msgid "Finished" msgstr "" -#: /home/kovid/work/calibre/src/calibre/utils/ipc/job.py:75 +#: /home/kovid/work/calibre/src/calibre/utils/ipc/job.py:76 msgid "Working..." msgstr "" From f45425ba99373489d86930f504dcf83a2f61ad6a Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 14 Aug 2010 23:17:32 -0600 Subject: [PATCH 15/73] Updated translations template --- src/calibre/gui2/actions/choose_library.py | 2 +- src/calibre/translations/calibre.pot | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/calibre/gui2/actions/choose_library.py b/src/calibre/gui2/actions/choose_library.py index 713136029d..52485c91b0 100644 --- a/src/calibre/gui2/actions/choose_library.py +++ b/src/calibre/gui2/actions/choose_library.py @@ -131,7 +131,7 @@ class ChooseLibraryAction(InterfaceAction): if not exists: warning_dialog(self.gui, _('No library found'), _('No existing calibre library was found at %s.' - ' It will be removed from the list of known ' + ' It will be removed from the list of known' ' libraries.')%loc, show=True) self.stats.remove(location) self.build_menus() diff --git a/src/calibre/translations/calibre.pot b/src/calibre/translations/calibre.pot index 5a92835907..ed1b0044d2 100644 --- a/src/calibre/translations/calibre.pot +++ b/src/calibre/translations/calibre.pot @@ -5,8 +5,8 @@ msgid "" msgstr "" "Project-Id-Version: calibre 0.7.14\n" -"POT-Creation-Date: 2010-08-14 23:13+MDT\n" -"PO-Revision-Date: 2010-08-14 23:13+MDT\n" +"POT-Creation-Date: 2010-08-14 23:17+MDT\n" +"PO-Revision-Date: 2010-08-14 23:17+MDT\n" "Last-Translator: Automatically generated\n" "Language-Team: LANGUAGE\n" "MIME-Version: 1.0\n" @@ -2679,7 +2679,7 @@ msgid "No library found" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:133 -msgid "No existing calibre library was found at %s. It will be removed from the list of known libraries." +msgid "No existing calibre library was found at %s. It will be removed from the list of known libraries." msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/actions/convert.py:22 From 7c7f5ec11972704b791ced6e1c9691693d4a3180 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 15 Aug 2010 03:08:42 -0600 Subject: [PATCH 16/73] ... --- src/calibre/gui2/convert/__init__.py | 2 +- src/calibre/gui2/dialogs/config/__init__.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/calibre/gui2/convert/__init__.py b/src/calibre/gui2/convert/__init__.py index 89cd372fad..c8f8733899 100644 --- a/src/calibre/gui2/convert/__init__.py +++ b/src/calibre/gui2/convert/__init__.py @@ -22,7 +22,7 @@ def config_widget_for_input_plugin(plugin): name = plugin.name.lower().replace(' ', '_') try: return __import__('calibre.gui2.convert.'+name, - fromlist=[1]) + fromlist=[1]).PluginWidget except ImportError: pass diff --git a/src/calibre/gui2/dialogs/config/__init__.py b/src/calibre/gui2/dialogs/config/__init__.py index 8739b22012..20c7843aa8 100644 --- a/src/calibre/gui2/dialogs/config/__init__.py +++ b/src/calibre/gui2/dialogs/config/__init__.py @@ -60,9 +60,8 @@ class ConfigTabs(QTabWidget): self.widgets = [lf, ps, sd, toc] for plugin in input_format_plugins(): - input_widget = config_widget_for_input_plugin(plugin) - if input_widget is not None: - pw = input_widget.PluginWidget + pw = config_widget_for_input_plugin(plugin) + if pw is not None: pw.ICON = I('forward.svg') self.widgets.append(widget_factory(pw)) From e9c1aa873c07dd90c4c0949740413c73a06cb269 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 15 Aug 2010 03:17:27 -0600 Subject: [PATCH 17/73] Do not allow new jobs to start when all cores are used. Also Fix #6130 (Conversion details show wrong conversion in title) --- src/calibre/gui2/jobs.py | 1 + src/calibre/utils/ipc/server.py | 13 ++++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/calibre/gui2/jobs.py b/src/calibre/gui2/jobs.py index 813a9bda45..4b00d39877 100644 --- a/src/calibre/gui2/jobs.py +++ b/src/calibre/gui2/jobs.py @@ -175,6 +175,7 @@ class JobManager(QAbstractTableModel): self.jobs.append(job) self.jobs.sort() self.job_added.emit(len(self.unfinished_jobs())) + self.layoutChanged.emit() def done_jobs(self): return [j for j in self.jobs if j.is_finished] diff --git a/src/calibre/utils/ipc/server.py b/src/calibre/utils/ipc/server.py index 6ac8df7a24..9f21cb638f 100644 --- a/src/calibre/utils/ipc/server.py +++ b/src/calibre/utils/ipc/server.py @@ -103,7 +103,7 @@ class Server(Thread): authkey=self.auth_key, backlog=4) self.add_jobs_queue, self.changed_jobs_queue = Queue(), Queue() self.kill_queue = Queue() - self.waiting_jobs, self.processing_jobs = [], deque() + self.waiting_jobs = [] self.pool, self.workers = deque(), deque() self.launched_worker_count = 0 self._worker_launch_lock = RLock() @@ -227,8 +227,15 @@ class Server(Thread): def suitable_waiting_job(self): available_workers = len(self.pool) - if available_workers == 0: - return None + for worker in self.workers: + job = worker.job + if job.core_usage == -1: + available_workers = 0 + elif job.core_usage > 1: + available_workers -= job.core_usage - 1 + if available_workers < 1: + return None + for i, job in enumerate(self.waiting_jobs): if job.core_usage == -1: if available_workers >= self.pool_size: From 45d20f7d7c7d20dfc585182c0630b6b2f2810689 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 15 Aug 2010 11:19:35 -0600 Subject: [PATCH 18/73] Show current library in title bar --- src/calibre/gui2/actions/__init__.py | 18 ++++++++++++++++-- src/calibre/gui2/actions/choose_library.py | 13 ++++++++++++- src/calibre/gui2/actions/fetch_news.py | 7 ++++++- src/calibre/gui2/ui.py | 14 ++++++++++---- 4 files changed, 44 insertions(+), 8 deletions(-) diff --git a/src/calibre/gui2/actions/__init__.py b/src/calibre/gui2/actions/__init__.py index c4d755aaea..bce1f283aa 100644 --- a/src/calibre/gui2/actions/__init__.py +++ b/src/calibre/gui2/actions/__init__.py @@ -26,8 +26,8 @@ class InterfaceAction(QObject): If two :class:`InterfaceAction` objects have the same name, the one with higher priority takes precedence. - Sub-classes should implement the :meth:`genesis` and - :meth:`location_selected` methods. + Sub-classes should implement the :meth:`genesis`, :meth:`library_moved`, + :meth:`location_selected` and :meth:`initialization_complete` methods. Once initialized, this plugin has access to the main calibre GUI via the :attr:`gui` member. You can access other plugins by name, for example:: @@ -108,3 +108,17 @@ class InterfaceAction(QObject): ''' pass + def library_changed(self, db): + ''' + Called whenever the current library is changed. + + :param db: The LibraryDatabase corresponding to the current library. + ''' + pass + + def initialization_complete(self): + ''' + Called once per action when the initialization of the main GUI is + completed. + ''' + pass diff --git a/src/calibre/gui2/actions/choose_library.py b/src/calibre/gui2/actions/choose_library.py index 52485c91b0..7bdbcf748a 100644 --- a/src/calibre/gui2/actions/choose_library.py +++ b/src/calibre/gui2/actions/choose_library.py @@ -97,10 +97,21 @@ class ChooseLibraryAction(InterfaceAction): ac.triggered.connect(partial(self.qs_requested, i)) self.choose_menu.addAction(ac) - def library_used(self, db): + def library_name(self): + db = self.gui.library_view.model().db + path = db.library_path + if isbytestring(path): + path = path.decode(filesystem_encoding) + path = path.replace(os.sep, '/') + return self.stats.pretty(path) + + def library_changed(self, db): self.stats.library_used(db) self.build_menus() + def initialization_complete(self): + self.library_changed(self.gui.library_view.model().db) + def build_menus(self): db = self.gui.library_view.model().db locations = list(self.stats.locations(db)) diff --git a/src/calibre/gui2/actions/fetch_news.py b/src/calibre/gui2/actions/fetch_news.py index bf44f5ab0e..b2893e0834 100644 --- a/src/calibre/gui2/actions/fetch_news.py +++ b/src/calibre/gui2/actions/fetch_news.py @@ -31,7 +31,12 @@ class FetchNewsAction(InterfaceAction): self.qaction.setMenu(self.scheduler.news_menu) self.qaction.triggered.connect( self.scheduler.show_dialog) - self.database_changed = self.scheduler.database_changed + + def library_changed(self, db): + self.scheduler.database_changed(db) + + def initialization_complete(self): + self.connect_scheduler() def connect_scheduler(self): self.scheduler.delete_old_news.connect( diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 820656d0e2..01e9d959f9 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -249,9 +249,10 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{ self.read_settings() self.finalize_layout() self.donate_button.start_animation() + self.set_window_title() - self.iactions['Fetch News'].connect_scheduler() - self.iactions['Choose Library'].library_used(self.library_view.model().db) + for ac in self.iactions.values(): + ac.initialization_complete() def start_content_server(self): from calibre.library.server.main import start_threaded_server @@ -367,9 +368,14 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{ self.saved_search.clear_to_help() self.book_details.reset_info() self.library_view.model().count_changed() - self.iactions['Fetch News'].database_changed(db) prefs['library_path'] = self.library_path - self.iactions['Choose Library'].library_used(self.library_view.model().db) + db = self.library_view.model().db + for action in self.iactions.values(): + action.library_changed(db) + self.set_window_title() + + def set_window_title(self): + self.setWindowTitle(__appname__ + u' - ||%s||'%self.iactions['Choose Library'].library_name()) def location_selected(self, location): From 51c56af0797a967b8b426b0e4f1135cb3e19bbff Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 15 Aug 2010 11:25:19 -0600 Subject: [PATCH 19/73] Add hooks to GUI plugins for the shutdown process --- src/calibre/gui2/actions/__init__.py | 14 +++++++++++++- src/calibre/gui2/ui.py | 6 +++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/calibre/gui2/actions/__init__.py b/src/calibre/gui2/actions/__init__.py index bce1f283aa..9c20f2adef 100644 --- a/src/calibre/gui2/actions/__init__.py +++ b/src/calibre/gui2/actions/__init__.py @@ -27,7 +27,8 @@ class InterfaceAction(QObject): priority takes precedence. Sub-classes should implement the :meth:`genesis`, :meth:`library_moved`, - :meth:`location_selected` and :meth:`initialization_complete` methods. + :meth:`location_selected` :meth:`shutting_down` + and :meth:`initialization_complete` methods. Once initialized, this plugin has access to the main calibre GUI via the :attr:`gui` member. You can access other plugins by name, for example:: @@ -122,3 +123,14 @@ class InterfaceAction(QObject): completed. ''' pass + + def shutting_down(self): + ''' + Called once per plugin when the main GUI is in the process of shutting + down. Release any used resources, but try not to block the shutdown for + long periods of time. + + :return: False to halt the shutdown. You are responsible for telling + the user why the shutdown was halted. + ''' + return True diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 01e9d959f9..1b918e679e 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -352,8 +352,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{ def booklists(self): return self.memory_view.model().db, self.card_a_view.model().db, self.card_b_view.model().db - - def library_moved(self, newloc): if newloc is None: return db = LibraryDatabase2(newloc) @@ -377,7 +375,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{ def set_window_title(self): self.setWindowTitle(__appname__ + u' - ||%s||'%self.iactions['Choose Library'].library_name()) - def location_selected(self, location): ''' Called when a location icon is clicked (e.g. Library) @@ -517,6 +514,9 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{ def shutdown(self, write_settings=True): + for action in self.iactions.values(): + if not action.shutting_down(): + return if write_settings: self.write_settings() self.check_messages_timer.stop() From c1bf0ebd13005f6bbefd1ce3119e5e4d75217764 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 15 Aug 2010 12:39:12 -0600 Subject: [PATCH 20/73] Futurismic by DM. Fixes #6514 (New recipe for SF blog Futurismic) --- resources/images/news/futurismic.png | Bin 0 -> 479 bytes resources/recipes/futurismic.recipe | 38 +++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 resources/images/news/futurismic.png create mode 100644 resources/recipes/futurismic.recipe diff --git a/resources/images/news/futurismic.png b/resources/images/news/futurismic.png new file mode 100644 index 0000000000000000000000000000000000000000..1f5870e8479f21142926285f22dd14b287804a93 GIT binary patch literal 479 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Y)RhkE)4%caKYZ?lYt_f1s;*b zK-vS0-A-oPfdtD69Mgd`SU*F|v9*U87#KM`T^vI!PQRVB*XxLbNbCNYXINsyTYVol zm2oGnUctZM2a{d%t)EhlxPP!uvpdigHf`HQr`#h>95-HcEjlsl?D?(SD%RZ_zn45W z_*}U2`-;}H&I^?U7s@Z^X!>Zf;6tyQW@v3!VePlIiMbmOzwvP}xTqlh@PWybr+;6U zJJ@(c&YGQ;p1hRBcIS?j>}-DT81>_$;e3_TCtc<=Eabdr_v=mR;h=5zc`G}rzA`8|%~`Tk$1iBpo@Xo! zyNdGHiu7^yu>EoU&v0FmG5gd;Q#s`-lRGZiHt!aP_nlkU_2%72$@^7)2l$gkBNv=) z)i? Date: Sun, 15 Aug 2010 14:30:01 -0600 Subject: [PATCH 21/73] Fix #6509 (v0.7.14 - Broken link in book description) --- src/calibre/gui2/dialogs/book_info.py | 6 +++--- src/calibre/gui2/dialogs/book_info.ui | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/calibre/gui2/dialogs/book_info.py b/src/calibre/gui2/dialogs/book_info.py index 9770ef864f..e085dcc3e2 100644 --- a/src/calibre/gui2/dialogs/book_info.py +++ b/src/calibre/gui2/dialogs/book_info.py @@ -48,11 +48,11 @@ class BookInfo(QDialog, Ui_BookInfo): self.refresh(row) def open_book_path(self, path): - if os.sep in unicode(path): + path = unicode(path) + if os.sep in path: open_local_file(path) else: - format = unicode(path) - path = self.view.model().db.format_abspath(self.current_row, format) + path = self.view.model().db.format_abspath(self.current_row, path) if path is not None: open_local_file(path) diff --git a/src/calibre/gui2/dialogs/book_info.ui b/src/calibre/gui2/dialogs/book_info.ui index 02dae12281..39f9abf3c5 100644 --- a/src/calibre/gui2/dialogs/book_info.ui +++ b/src/calibre/gui2/dialogs/book_info.ui @@ -57,7 +57,7 @@ - Fit &cover to view + Fit &cover within view From 388d8e34dcc16fa1d3410d3671c1c6440e6cef23 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 15 Aug 2010 14:56:17 -0600 Subject: [PATCH 22/73] Fix #6505 (Flesh out window settings and keyboard shortcuts.) --- src/calibre/gui2/cover_flow.py | 1 + src/calibre/gui2/jobs.py | 21 +++++++++++++++++++-- src/calibre/gui2/ui.py | 2 +- src/calibre/gui2/widgets.py | 9 ++++++--- 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/calibre/gui2/cover_flow.py b/src/calibre/gui2/cover_flow.py index b0252210e3..88bbae6c41 100644 --- a/src/calibre/gui2/cover_flow.py +++ b/src/calibre/gui2/cover_flow.py @@ -196,6 +196,7 @@ class CoverFlowMixin(object): def show_cover_browser(self): d = CBDialog(self, self.cover_flow) + d.addAction(self.cb_splitter.action_toggle) self.cover_flow.setVisible(True) self.cover_flow.setFocus(Qt.OtherFocusReason) d.show() diff --git a/src/calibre/gui2/jobs.py b/src/calibre/gui2/jobs.py index 4b00d39877..e92a502daa 100644 --- a/src/calibre/gui2/jobs.py +++ b/src/calibre/gui2/jobs.py @@ -14,7 +14,8 @@ from Queue import Empty, Queue from PyQt4.Qt import QAbstractTableModel, QVariant, QModelIndex, Qt, \ QTimer, pyqtSignal, QIcon, QDialog, QAbstractItemDelegate, QApplication, \ QSize, QStyleOptionProgressBarV2, QString, QStyle, QToolTip, QFrame, \ - QHBoxLayout, QVBoxLayout, QSizePolicy, QLabel, QCoreApplication + QHBoxLayout, QVBoxLayout, QSizePolicy, QLabel, QCoreApplication, QAction, \ + QByteArray from calibre.utils.ipc.server import Server from calibre.utils.ipc.job import ParallelJob @@ -281,6 +282,7 @@ class JobsButton(QFrame): self.pi = ProgressIndicator(self, size) self._jobs = QLabel(''+_('Jobs:')+' 0') self._jobs.mouseReleaseEvent = self.mouseReleaseEvent + self.shortcut = _('Shift+Alt+J') if horizontal: self.setLayout(QHBoxLayout()) @@ -297,15 +299,24 @@ class JobsButton(QFrame): self.layout().setMargin(0) self._jobs.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) self.setCursor(Qt.PointingHandCursor) - self.setToolTip(_('Click to see list of active jobs.')) + b = _('Click to see list of jobs') + self.setToolTip(b + u' (%s)'%self.shortcut) + self.action_toggle = QAction(b, parent) + parent.addAction(self.action_toggle) + self.action_toggle.setShortcut(self.shortcut) + self.action_toggle.triggered.connect(self.toggle) def initialize(self, jobs_dialog, job_manager): self.jobs_dialog = jobs_dialog job_manager.job_added.connect(self.job_added) job_manager.job_done.connect(self.job_done) + self.jobs_dialog.addAction(self.action_toggle) def mouseReleaseEvent(self, event): + self.toggle() + + def toggle(self, *args): if self.jobs_dialog.isVisible(): self.jobs_dialog.hide() else: @@ -372,6 +383,10 @@ class JobsDialog(QDialog, Ui_JobsDialog): except: pass + geom = gprefs.get('jobs_dialog_geometry', bytearray('')) + self.restoreGeometry(QByteArray(geom)) + + def show_job_details(self, index): row = index.row() job = self.jobs_view.model().row_to_job(row) @@ -397,6 +412,8 @@ class JobsDialog(QDialog, Ui_JobsDialog): try: state = bytearray(self.jobs_view.horizontalHeader().saveState()) gprefs['jobs view column layout'] = state + geom = bytearray(self.saveGeometry()) + gprefs['jobs_dialog_geometry'] = geom except: pass e.accept() diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 1b918e679e..95cccb72a5 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -132,7 +132,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{ # Jobs Button {{{ self.job_manager = JobManager() self.jobs_dialog = JobsDialog(self, self.job_manager) - self.jobs_button = JobsButton(horizontal=True) + self.jobs_button = JobsButton(horizontal=True, parent=self) self.jobs_button.initialize(self.jobs_dialog, self.job_manager) # }}} diff --git a/src/calibre/gui2/widgets.py b/src/calibre/gui2/widgets.py index 54cb526750..dbb3947d4c 100644 --- a/src/calibre/gui2/widgets.py +++ b/src/calibre/gui2/widgets.py @@ -871,14 +871,14 @@ class LayoutButton(QToolButton): def set_state_to_show(self, *args): self.setChecked(False) label =_('Show') - self.setText(label + ' ' + self.label + ' ' + self.shortcut) + self.setText(label + ' ' + self.label + u' (%s)'%self.shortcut) self.setToolTip(self.text()) self.setStatusTip(self.text()) def set_state_to_hide(self, *args): self.setChecked(True) label = _('Hide') - self.setText(label + ' ' + self.label+ ' ' + self.shortcut) + self.setText(label + ' ' + self.label+ u' (%s)'%self.shortcut) self.setToolTip(self.text()) self.setStatusTip(self.text()) @@ -941,7 +941,10 @@ class Splitter(QSplitter): @property def is_side_index_hidden(self): sizes = list(self.sizes()) - return sizes[self.side_index] == 0 + try: + return sizes[self.side_index] == 0 + except IndexError: + return True @property def save_name(self): From 4677b9acc129bb92e3134efa9443871014b99836 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 15 Aug 2010 15:02:24 -0600 Subject: [PATCH 23/73] ... --- src/calibre/gui2/jobs.py | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/calibre/gui2/jobs.py b/src/calibre/gui2/jobs.py index e92a502daa..fed101d278 100644 --- a/src/calibre/gui2/jobs.py +++ b/src/calibre/gui2/jobs.py @@ -376,15 +376,19 @@ class JobsDialog(QDialog, Ui_JobsDialog): self.jobs_view.setItemDelegateForColumn(2, self.pb_delegate) self.jobs_view.doubleClicked.connect(self.show_job_details) self.jobs_view.horizontalHeader().setMovable(True) - state = gprefs.get('jobs view column layout', None) - if state is not None: - try: - self.jobs_view.horizontalHeader().restoreState(bytes(state)) - except: - pass + self.restore_state() + def restore_state(self): geom = gprefs.get('jobs_dialog_geometry', bytearray('')) self.restoreGeometry(QByteArray(geom)) + state = gprefs.get('jobs view column layout', bytearray('')) + self.jobs_view.horizontalHeader().restoreState(QByteArray(state)) + + def save_state(self): + state = bytearray(self.jobs_view.horizontalHeader().saveState()) + gprefs['jobs view column layout'] = state + geom = bytearray(self.saveGeometry()) + gprefs['jobs_dialog_geometry'] = geom def show_job_details(self, index): @@ -409,11 +413,13 @@ class JobsDialog(QDialog, Ui_JobsDialog): self.model.kill_all_jobs() def closeEvent(self, e): - try: - state = bytearray(self.jobs_view.horizontalHeader().saveState()) - gprefs['jobs view column layout'] = state - geom = bytearray(self.saveGeometry()) - gprefs['jobs_dialog_geometry'] = geom - except: - pass - e.accept() + self.save_state() + return QDialog.closeEvent(self, e) + + def show(self, *args): + self.restore_state() + return QDialog.show(self, *args) + + def hide(self, *args): + self.save_state() + return QDialog.hide(self, *args) From aa371fed65f5e2a748bcbe656aed762bc7169b13 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 15 Aug 2010 15:02:54 -0600 Subject: [PATCH 24/73] ... --- src/calibre/gui2/jobs.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/calibre/gui2/jobs.py b/src/calibre/gui2/jobs.py index fed101d278..fd23234c8a 100644 --- a/src/calibre/gui2/jobs.py +++ b/src/calibre/gui2/jobs.py @@ -379,16 +379,22 @@ class JobsDialog(QDialog, Ui_JobsDialog): self.restore_state() def restore_state(self): - geom = gprefs.get('jobs_dialog_geometry', bytearray('')) - self.restoreGeometry(QByteArray(geom)) - state = gprefs.get('jobs view column layout', bytearray('')) - self.jobs_view.horizontalHeader().restoreState(QByteArray(state)) + try: + geom = gprefs.get('jobs_dialog_geometry', bytearray('')) + self.restoreGeometry(QByteArray(geom)) + state = gprefs.get('jobs view column layout', bytearray('')) + self.jobs_view.horizontalHeader().restoreState(QByteArray(state)) + except: + pass def save_state(self): - state = bytearray(self.jobs_view.horizontalHeader().saveState()) - gprefs['jobs view column layout'] = state - geom = bytearray(self.saveGeometry()) - gprefs['jobs_dialog_geometry'] = geom + try: + state = bytearray(self.jobs_view.horizontalHeader().saveState()) + gprefs['jobs view column layout'] = state + geom = bytearray(self.saveGeometry()) + gprefs['jobs_dialog_geometry'] = geom + except: + pass def show_job_details(self, index): From 61f0cbe23c57abfdfb1449982ce4e8cde2c913cb Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 15 Aug 2010 16:05:00 -0600 Subject: [PATCH 25/73] Fix #6511 (Tag browser problem after switching libraries) --- src/calibre/gui2/tag_view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index 1565520ca1..239ee6a35a 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -78,6 +78,7 @@ class TagsView(QTreeView): # {{{ self.setAnimated(True) self.setHeaderHidden(True) self.setItemDelegate(TagDelegate(self)) + self.clicked.connect(self.toggle) def set_database(self, db, tag_match, sort_by): self.hidden_categories = config['tag_browser_hidden_categories'] @@ -90,7 +91,6 @@ class TagsView(QTreeView): # {{{ self.search_restriction = None self.setModel(self._model) self.setContextMenuPolicy(Qt.CustomContextMenu) - self.clicked.connect(self.toggle) self.customContextMenuRequested.connect(self.show_context_menu) pop = config['sort_tags_by'] self.sort_by.setCurrentIndex(self.db.CATEGORY_SORTS.index(pop)) From b4c94461579f0db41afb79e9eeb90727a422c702 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 15 Aug 2010 16:58:24 -0600 Subject: [PATCH 26/73] Only make connections between tag browser components once when switching libraries --- src/calibre/gui2/tag_view.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index 239ee6a35a..7d2333f58b 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -78,7 +78,7 @@ class TagsView(QTreeView): # {{{ self.setAnimated(True) self.setHeaderHidden(True) self.setItemDelegate(TagDelegate(self)) - self.clicked.connect(self.toggle) + self.made_connections = False def set_database(self, db, tag_match, sort_by): self.hidden_categories = config['tag_browser_hidden_categories'] @@ -91,11 +91,14 @@ class TagsView(QTreeView): # {{{ self.search_restriction = None self.setModel(self._model) self.setContextMenuPolicy(Qt.CustomContextMenu) - self.customContextMenuRequested.connect(self.show_context_menu) pop = config['sort_tags_by'] self.sort_by.setCurrentIndex(self.db.CATEGORY_SORTS.index(pop)) - self.sort_by.currentIndexChanged.connect(self.sort_changed) - self.refresh_required.connect(self.recount, type=Qt.QueuedConnection) + if not self.made_connections: + self.clicked.connect(self.toggle) + self.customContextMenuRequested.connect(self.show_context_menu) + self.refresh_required.connect(self.recount, type=Qt.QueuedConnection) + self.sort_by.currentIndexChanged.connect(self.sort_changed) + self.made_connections = True db.add_listener(self.database_changed) def database_changed(self, event, ids): From 0497085c65164edf50000450b302e087653df743 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 16 Aug 2010 11:18:59 +0100 Subject: [PATCH 27/73] Improve bulk metadata performance when no changes to in_multiple columns --- src/calibre/gui2/custom_column_widgets.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/calibre/gui2/custom_column_widgets.py b/src/calibre/gui2/custom_column_widgets.py index 96232fe85f..1c72511630 100644 --- a/src/calibre/gui2/custom_column_widgets.py +++ b/src/calibre/gui2/custom_column_widgets.py @@ -407,7 +407,9 @@ class BulkBase(Base): if self.process_each_book(): for book_id in book_ids: val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True) - self.db.set_custom(book_id, self.getter(val), num=self.col_id, notify=notify) + new_val = self.getter(val) + if set(val) != new_val or True: + self.db.set_custom(book_id, new_val, num=self.col_id, notify=notify) else: val = self.getter() val = self.normalize_ui_val(val) @@ -544,8 +546,9 @@ class BulkText(BulkBase): ans = set(original_value) ans -= set([v.strip() for v in unicode(self.removing_widget.tags_box.text()).split(',')]) - ans |= set([v.strip() for v in - unicode(self.adding_widget.text()).split(',')]) + txt = unicode(self.adding_widget.text()) + if txt: + ans |= set([v.strip() for v in txt.split(',')]) return ans # returning a set instead of a list works, for now at least. val = unicode(self.widgets[1].currentText()).strip() if not val: From c917f95c2c723d81946fa417941edc4bf3bde687 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 16 Aug 2010 11:24:34 +0100 Subject: [PATCH 28/73] Take out debug test. --- src/calibre/gui2/custom_column_widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/custom_column_widgets.py b/src/calibre/gui2/custom_column_widgets.py index 1c72511630..0b15fcb633 100644 --- a/src/calibre/gui2/custom_column_widgets.py +++ b/src/calibre/gui2/custom_column_widgets.py @@ -408,7 +408,7 @@ class BulkBase(Base): for book_id in book_ids: val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True) new_val = self.getter(val) - if set(val) != new_val or True: + if set(val) != new_val: self.db.set_custom(book_id, new_val, num=self.col_id, notify=notify) else: val = self.getter() From d13d0342ba0d42934739e3af48c1c2ffc7369e35 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 16 Aug 2010 09:33:59 -0600 Subject: [PATCH 29/73] ... --- src/calibre/customize/builtins.py | 7 ++++++- src/calibre/gui2/actions/copy_to_library.py | 21 +++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 src/calibre/gui2/actions/copy_to_library.py diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index cd2896f232..35d7c846d0 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -657,9 +657,14 @@ class ActionEditCollections(InterfaceActionBase): name = 'Edit Collections' actual_plugin = 'calibre.gui2.actions.edit_collections:EditCollectionsAction' +class ActionCopyToLibrary(InterfaceActionBase): + name = 'Copy To Library' + actual_plugin = 'calibre.gui2.actions.copy_to_library:CopyToLibraryAction' + plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog, ActionConvert, ActionDelete, ActionEditMetadata, ActionView, ActionFetchNews, ActionSaveToDisk, ActionShowBookDetails, ActionRestart, ActionOpenFolder, ActionConnectShare, ActionSendToDevice, ActionHelp, ActionPreferences, ActionSimilarBooks, - ActionAddToLibrary, ActionEditCollections, ActionChooseLibrary] + ActionAddToLibrary, ActionEditCollections, ActionChooseLibrary, + ActionCopyToLibrary] diff --git a/src/calibre/gui2/actions/copy_to_library.py b/src/calibre/gui2/actions/copy_to_library.py new file mode 100644 index 0000000000..5b9e885cec --- /dev/null +++ b/src/calibre/gui2/actions/copy_to_library.py @@ -0,0 +1,21 @@ +#!/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' + +from PyQt4.Qt import QMenu + +from calibre.gui2.actions import InterfaceAction + +class CopyToLibraryAction(InterfaceAction): + + name = 'Copy To Library' + action_spec = (_('Copy to library'), 'lt.png', + _('Copy selected books to the specified library'), None) + + def genesis(self): + self.menu = QMenu(self.gui) + + From 3b52187aeedc91659f61b3b118a3db7de4a4a83b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 16 Aug 2010 09:50:25 -0600 Subject: [PATCH 30/73] Implement #6523 (New value for existing tweak) --- resources/default_tweaks.py | 1 + src/calibre/ebooks/metadata/__init__.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/default_tweaks.py b/resources/default_tweaks.py index 80456234b9..d6f134f724 100644 --- a/resources/default_tweaks.py +++ b/resources/default_tweaks.py @@ -24,6 +24,7 @@ series_index_auto_increment = 'next' # invert: use "fn ln" -> "ln, fn" (the original algorithm) # copy : copy author to author_sort without modification # comma : use 'copy' if there is a ',' in the name, otherwise use 'invert' +# nocomma : "fn ln" -> "ln fn" (without the comma) author_sort_copy_method = 'invert' diff --git a/src/calibre/ebooks/metadata/__init__.py b/src/calibre/ebooks/metadata/__init__.py index 1e62cffd0f..8ce81c73d5 100644 --- a/src/calibre/ebooks/metadata/__init__.py +++ b/src/calibre/ebooks/metadata/__init__.py @@ -38,7 +38,7 @@ def author_to_author_sort(author): author = _bracket_pat.sub('', author).strip() tokens = author.split() tokens = tokens[-1:] + tokens[:-1] - if len(tokens) > 1: + if len(tokens) > 1 and method != 'nocomma': tokens[0] += ',' return ' '.join(tokens) From 56d5f9f2a9cdee2c2f1c61b4bee9d1625a49b1be Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 16 Aug 2010 16:34:51 -0600 Subject: [PATCH 31/73] Fix #6530 (Add Support for Samsung Captivate) --- 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 951ed66e72..969fdfbd19 100644 --- a/src/calibre/devices/android/driver.py +++ b/src/calibre/devices/android/driver.py @@ -55,9 +55,9 @@ class ANDROID(USBMS): VENDOR_NAME = ['HTC', 'MOTOROLA', 'GOOGLE_', 'ANDROID', 'ACER', 'GT-I5700', 'SAMSUNG', 'DELL', 'LINUX'] WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE', - '__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', + '__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', 'SGH-I897', 'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID'] - WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', + WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID'] OSX_MAIN_MEM = 'HTC Android Phone Media' From 6bbc5c3e2b67b9ea082b7ec4422c58530cbde17a Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 16 Aug 2010 16:35:33 -0600 Subject: [PATCH 32/73] ... --- src/calibre/devices/hanlin/driver.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/calibre/devices/hanlin/driver.py b/src/calibre/devices/hanlin/driver.py index f19135a7e7..10dd333b8d 100644 --- a/src/calibre/devices/hanlin/driver.py +++ b/src/calibre/devices/hanlin/driver.py @@ -80,6 +80,13 @@ class HANLINV3(USBMS): drives['carda'] = main return drives +class SPECTRA(HANLINV3): + + name = 'Spectra' + gui_name = 'Spectra' + PRODUCT_ID = [0xa4a5] + + FORMATS = ['epub', 'mobi', 'fb2', 'lit', 'prc', 'djvu', 'pdf', 'rtf', 'txt'] class HANLINV5(HANLINV3): name = 'Hanlin V5 driver' From 8b40b9f22c81e90060c8d09334cad3dfeed4ab74 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 16 Aug 2010 19:13:16 -0600 Subject: [PATCH 33/73] DB: Store temporary tables in memory. Fix #6472 (Add/change tags for large number of eBooks is s l o w) --- src/calibre/gui2/dialogs/metadata_bulk.py | 18 +++-- src/calibre/library/database2.py | 86 +++++++++++++++++++++-- src/calibre/library/sqlite.py | 18 +++++ 3 files changed, 106 insertions(+), 16 deletions(-) diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index 9fcfe13253..05c4f48cf3 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -27,8 +27,9 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): self.changed = False QObject.connect(self.button_box, SIGNAL("accepted()"), self.sync) - self.tags.update_tags_cache(self.db.all_tags()) - self.remove_tags.update_tags_cache(self.db.all_tags()) + all_tags = self.db.all_tags() + self.tags.update_tags_cache(all_tags) + self.remove_tags.update_tags_cache(all_tags) self.initialize_combos() @@ -103,6 +104,11 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): self.remove_tags.update_tags_cache(self.db.all_tags()) def sync(self): + remove = unicode(self.remove_tags.text()).strip().split(',') + add = unicode(self.tags.text()).strip().split(',') + self.db.bulk_modify_tags(self.ids, add=add, remove=remove) + + for id in self.ids: au = unicode(self.authors.text()) if au: @@ -120,14 +126,6 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): pub = unicode(self.publisher.text()) if pub: self.db.set_publisher(id, pub, notify=False) - remove_tags = unicode(self.remove_tags.text()).strip() - if remove_tags: - remove_tags = [i.strip() for i in remove_tags.split(',')] - self.db.unapply_tags(id, remove_tags, notify=False) - tags = unicode(self.tags.text()).strip() - if tags: - tags = map(lambda x: x.strip(), tags.split(',')) - self.db.set_tags(id, tags, append=True, notify=False) if self.write_series: series = unicode(self.series.currentText()).strip() next = self.db.get_next_series_num_for(series) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 36a31b78a2..b8ac065760 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -26,7 +26,7 @@ from calibre.ebooks.metadata.meta import get_metadata, metadata_from_formats from calibre.constants import preferred_encoding, iswindows, isosx, filesystem_encoding from calibre.ptempfile import PersistentTemporaryFile from calibre.customize.ui import run_plugins_on_import - +from calibre import isbytestring from calibre.utils.filenames import ascii_filename from calibre.utils.date import utcnow, now as nowf, utcfromtimestamp from calibre.utils.config import prefs, tweaks @@ -116,6 +116,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): # so that various code taht connects directly will not complain about # missing functions self.books_list_filter = self.conn.create_dynamic_filter('books_list_filter') + # Store temporary tables in memory + self.conn.execute('pragma temp_store=2') + self.conn.commit() @classmethod def exists_at(cls, path): @@ -1369,6 +1372,80 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): return set([]) return set([r[0] for r in result]) + @classmethod + def cleanup_tags(cls, tags): + tags = [x.strip() for x in tags if x.strip()] + tags = [x.decode(preferred_encoding, 'replace') \ + if isbytestring(x) else x for x in tags] + tags = [u' '.join(x.split()) for x in tags] + ans, seen = [], set([]) + for tag in tags: + if tag.lower() not in seen: + seen.add(tag.lower()) + ans.append(tag) + return ans + + def bulk_modify_tags(self, ids, add=[], remove=[], notify=False): + add = self.cleanup_tags(add) + remove = self.cleanup_tags(remove) + remove = set(remove) - set(add) + if not ids or (not add and not remove): + return + + # Add tags that do not already exist into the tag table + all_tags = self.all_tags() + lt = [t.lower() for t in all_tags] + new_tags = [t for t in add if t.lower() not in lt] + if new_tags: + self.conn.executemany('INSERT INTO tags(name) VALUES (?)', [(x,) for x in + new_tags]) + + # Create the temporary tables to store the ids for books and tags + # to be operated on + tables = ('temp_bulk_tag_edit_books', 'temp_bulk_tag_edit_add', + 'temp_bulk_tag_edit_remove') + drops = '\n'.join(['DROP TABLE IF EXISTS %s;'%t for t in tables]) + creates = '\n'.join(['CREATE TEMP TABLE %s(id INTEGER PRIMARY KEY);'%t + for t in tables]) + self.conn.executescript(drops + creates) + + # Populate the books temp table + self.conn.executemany( + 'INSERT INTO temp_bulk_tag_edit_books VALUES (?)', + [(x,) for x in ids]) + + # Populate the add/remove tags temp tables + for table, tags in enumerate([add, remove]): + if not tags: + continue + table = tables[table+1] + insert = ('INSERT INTO %s(id) SELECT tags.id FROM tags WHERE name=?' + ' COLLATE PYNOCASE LIMIT 1') + self.conn.executemany(insert%table, [(x,) for x in tags]) + + if remove: + self.conn.execute( + '''DELETE FROM books_tags_link WHERE + book IN (SELECT id FROM %s) AND + tag IN (SELECT id FROM %s)''' + % (tables[0], tables[2])) + + if add: + self.conn.execute( + ''' + INSERT INTO books_tags_link(book, tag) SELECT {0}.id, {1}.id FROM + {0}, {1} + '''.format(tables[0], tables[1]) + ) + self.conn.executescript(drops) + self.conn.commit() + + for x in ids: + tags = u','.join(self.get_tags(x)) + self.data.set(x, self.FIELD_MAP['tags'], tags, row_is_id=True) + if notify: + self.notify('metadata', ids) + def set_tags(self, id, tags, append=False, notify=True): ''' @param tags: list of strings @@ -1378,10 +1455,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.conn.execute('DELETE FROM books_tags_link WHERE book=?', (id,)) self.conn.execute('DELETE FROM tags WHERE (SELECT COUNT(id) FROM books_tags_link WHERE tag=tags.id) < 1') otags = self.get_tags(id) - tags = [x.strip() for x in tags if x.strip()] - tags = [x.decode(preferred_encoding, 'replace') if not isinstance(x, - unicode) else x for x in tags] - tags = [u' '.join(x.split()) for x in tags] + tags = self.cleanup_tags(tags) for tag in (set(tags)-otags): tag = tag.strip() if not tag: @@ -1407,7 +1481,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.conn.execute('INSERT INTO books_tags_link(book, tag) VALUES (?,?)', (id, tid)) self.conn.commit() - tags = ','.join(self.get_tags(id)) + tags = u','.join(self.get_tags(id)) self.data.set(id, self.FIELD_MAP['tags'], tags, row_is_id=True) if notify: self.notify('metadata', [id]) diff --git a/src/calibre/library/sqlite.py b/src/calibre/library/sqlite.py index 85954f6e0f..1242d0bf7b 100644 --- a/src/calibre/library/sqlite.py +++ b/src/calibre/library/sqlite.py @@ -13,10 +13,12 @@ from threading import Thread from Queue import Queue from threading import RLock from datetime import datetime +from functools import partial from calibre.ebooks.metadata import title_sort, author_to_author_sort from calibre.utils.config import tweaks from calibre.utils.date import parse_date, isoformat +from calibre import isbytestring global_lock = RLock() @@ -98,6 +100,19 @@ def _author_to_author_sort(x): if not x: return '' return author_to_author_sort(x.replace('|', ',')) +def pynocase(one, two, encoding='utf-8'): + if isbytestring(one): + try: + one = one.decode(encoding, 'replace') + except: + pass + if isbytestring(two): + try: + two = two.decode(encoding, 'replace') + except: + pass + return cmp(one.lower(), two.lower()) + class DBThread(Thread): CLOSE = '-------close---------' @@ -115,10 +130,13 @@ class DBThread(Thread): def connect(self): self.conn = sqlite.connect(self.path, factory=Connection, detect_types=sqlite.PARSE_DECLTYPES|sqlite.PARSE_COLNAMES) + encoding = self.conn.execute('pragma encoding').fetchone()[0] self.conn.row_factory = sqlite.Row if self.row_factory else lambda cursor, row : list(row) self.conn.create_aggregate('concat', 1, Concatenate) self.conn.create_aggregate('sortconcat', 2, SortedConcatenate) self.conn.create_aggregate('sort_concat', 2, SafeSortedConcatenate) + self.conn.create_collation('PYNOCASE', partial(pynocase, + encoding=encoding)) if tweaks['title_series_sorting'] == 'strictly_alphabetic': self.conn.create_function('title_sort', 1, lambda x:x) else: From 8d9a4164b48dad95a8bda9180e0d5488edf5f9d4 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 16 Aug 2010 23:19:23 -0600 Subject: [PATCH 34/73] Show progress dialog to give user feedback that something is happening when applying bulk metadata edit --- resources/recipes/science_aas.recipe | 2 +- src/calibre/gui2/actions/edit_metadata.py | 10 +- src/calibre/gui2/custom_column_widgets.py | 6 +- src/calibre/gui2/dialogs/metadata_bulk.py | 133 +++++++++++++++------- 4 files changed, 103 insertions(+), 48 deletions(-) diff --git a/resources/recipes/science_aas.recipe b/resources/recipes/science_aas.recipe index 092db8665e..d5f95c0b83 100644 --- a/resources/recipes/science_aas.recipe +++ b/resources/recipes/science_aas.recipe @@ -22,7 +22,7 @@ class ScienceAAS(BasicNewsRecipe): timefmt = ' [%A, %d %B, %Y]' needs_subscription = True LOGIN = 'http://www.sciencemag.org/cgi/login?uri=%2Findex.dtl' - + def get_browser(self): br = BasicNewsRecipe.get_browser() if self.username is not None and self.password is not None: diff --git a/src/calibre/gui2/actions/edit_metadata.py b/src/calibre/gui2/actions/edit_metadata.py index 05b4bdf7fc..9d1066ee40 100644 --- a/src/calibre/gui2/actions/edit_metadata.py +++ b/src/calibre/gui2/actions/edit_metadata.py @@ -174,8 +174,14 @@ class EditMetadataAction(InterfaceAction): _('No books selected')) d.exec_() return - if MetadataBulkDialog(self.gui, rows, - self.gui.library_view.model().db).changed: + # Prevent the TagView from updating due to signals from the database + self.gui.tags_view.blockSignals(True) + try: + changed = MetadataBulkDialog(self.gui, rows, + self.gui.library_view.model().db).changed + finally: + self.gui.tags_view.blockSignals(False) + if changed: self.gui.library_view.model().resort(reset=False) self.gui.library_view.model().research() self.gui.tags_view.recount() diff --git a/src/calibre/gui2/custom_column_widgets.py b/src/calibre/gui2/custom_column_widgets.py index 0b15fcb633..3ed7d0c4ad 100644 --- a/src/calibre/gui2/custom_column_widgets.py +++ b/src/calibre/gui2/custom_column_widgets.py @@ -11,7 +11,7 @@ from functools import partial from PyQt4.Qt import QComboBox, QLabel, QSpinBox, QDoubleSpinBox, QDateEdit, \ QDate, QGroupBox, QVBoxLayout, QPlainTextEdit, QSizePolicy, \ QSpacerItem, QIcon, QCheckBox, QWidget, QHBoxLayout, SIGNAL, \ - QPushButton + QPushButton, QCoreApplication from calibre.utils.date import qt_to_dt, now from calibre.gui2.widgets import TagsLineEdit, EnComboBox @@ -406,6 +406,7 @@ class BulkBase(Base): def commit(self, book_ids, notify=False): if self.process_each_book(): for book_id in book_ids: + QCoreApplication.processEvents() val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True) new_val = self.getter(val) if set(val) != new_val: @@ -415,6 +416,7 @@ class BulkBase(Base): val = self.normalize_ui_val(val) if val != self.initial_val: for book_id in book_ids: + QCoreApplication.processEvents() self.db.set_custom(book_id, val, num=self.col_id, notify=notify) class BulkBool(BulkBase, Bool): @@ -433,6 +435,7 @@ class BulkDateTime(BulkBase, DateTime): pass class BulkSeries(BulkBase): + def setup_ui(self, parent): values = self.all_values = list(self.db.all_custom(num=self.col_id)) values.sort(cmp = lambda x,y: cmp(x.lower(), y.lower())) @@ -458,6 +461,7 @@ class BulkSeries(BulkBase): update_indices = self.idx_widget.checkState() if val != '': for book_id in book_ids: + QCoreApplication.processEvents() if update_indices: if tweaks['series_index_auto_increment'] == 'next': s_index = self.db.get_next_cc_series_num_for\ diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index 05c4f48cf3..dac3e3f477 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -3,8 +3,8 @@ __copyright__ = '2008, Kovid Goyal ' '''Dialog to edit metadata in bulk''' -from PyQt4.QtCore import SIGNAL, QObject -from PyQt4.QtGui import QDialog, QGridLayout +from PyQt4.Qt import SIGNAL, QObject, QDialog, QGridLayout, \ + QProgressDialog, QCoreApplication, QString from calibre.gui2.dialogs.metadata_bulk_ui import Ui_MetadataBulkDialog from calibre.gui2.dialogs.tag_editor import TagEditor @@ -25,7 +25,6 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): len(rows)) self.write_series = False self.changed = False - QObject.connect(self.button_box, SIGNAL("accepted()"), self.sync) all_tags = self.db.all_tags() self.tags.update_tags_cache(all_tags) @@ -103,56 +102,102 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): self.tags.update_tags_cache(self.db.all_tags()) self.remove_tags.update_tags_cache(self.db.all_tags()) - def sync(self): - remove = unicode(self.remove_tags.text()).strip().split(',') - add = unicode(self.tags.text()).strip().split(',') - self.db.bulk_modify_tags(self.ids, add=add, remove=remove) + def accept(self): + if len(self.ids) < 1: + return QDialog.accept(self) + pd = QProgressDialog( + _('Applying changes to %d books. This may take a while.')%len(self.ids), + QString(), 0, 0, self) + pd.setModal(True) + pd.show() + pd.setValue(0) + def upd(): + QCoreApplication.processEvents() - for id in self.ids: + try: + remove = unicode(self.remove_tags.text()).strip().split(',') + add = unicode(self.tags.text()).strip().split(',') au = unicode(self.authors.text()) - if au: - au = string_to_authors(au) - self.db.set_authors(id, au, notify=False) - if self.auto_author_sort.isChecked(): - x = self.db.author_sort_from_book(id, index_is_id=True) - if x: - self.db.set_author_sort(id, x, notify=False) aus = unicode(self.author_sort.text()) - if aus and self.author_sort.isEnabled(): - self.db.set_author_sort(id, aus, notify=False) - if self.rating.value() != -1: - self.db.set_rating(id, 2*self.rating.value(), notify=False) + do_aus = self.author_sort.isEnabled() + rating = self.rating.value() pub = unicode(self.publisher.text()) - if pub: - self.db.set_publisher(id, pub, notify=False) - if self.write_series: - series = unicode(self.series.currentText()).strip() - next = self.db.get_next_series_num_for(series) - self.db.set_series(id, series, notify=False) - num = next if self.autonumber_series.isChecked() and series else 1.0 - self.db.set_series_index(id, num, notify=False) + do_series = self.write_series + series = unicode(self.series.currentText()).strip() + do_autonumber = self.autonumber_series.isChecked() + do_remove_format = self.remove_format.currentIndex() > -1 + remove_format = unicode(self.remove_format.currentText()) + do_swap_ta = self.swap_title_and_author.isChecked() + do_remove_conv = self.remove_conversion_settings.isChecked() + do_auto_author = self.auto_author_sort.isChecked() - if self.remove_format.currentIndex() > -1: - self.db.remove_format(id, unicode(self.remove_format.currentText()), index_is_id=True, notify=False) + upd() + self.changed = bool(self.ids) + for id in self.ids: + upd() + if do_swap_ta: + title = self.db.title(id, index_is_id=True) + aum = self.db.authors(id, index_is_id=True) + if aum: + aum = [a.strip().replace('|', ',') for a in aum.split(',')] + new_title = authors_to_string(aum) + self.db.set_title(id, new_title, notify=False) + if title: + new_authors = string_to_authors(title) + self.db.set_authors(id, new_authors, notify=False) + upd() - if self.swap_title_and_author.isChecked(): - title = self.db.title(id, index_is_id=True) - aum = self.db.authors(id, index_is_id=True) - if aum: - aum = [a.strip().replace('|', ',') for a in aum.split(',')] - new_title = authors_to_string(aum) - self.db.set_title(id, new_title, notify=False) - if title: - new_authors = string_to_authors(title) - self.db.set_authors(id, new_authors, notify=False) + if au: + self.db.set_authors(id, string_to_authors(au), notify=False) + upd() - if self.remove_conversion_settings.isChecked(): - self.db.delete_conversion_options(id, 'PIPE') + if do_auto_author: + x = self.db.author_sort_from_book(id, index_is_id=True) + if x: + self.db.set_author_sort(id, x, notify=False) + upd() - self.changed = True - for w in getattr(self, 'custom_column_widgets', []): - w.commit(self.ids) + if aus and do_aus: + self.db.set_author_sort(id, aus, notify=False) + upd() + + if rating != -1: + self.db.set_rating(id, 2*rating, notify=False) + upd() + + if pub: + self.db.set_publisher(id, pub, notify=False) + upd() + + if do_series: + next = self.db.get_next_series_num_for(series) + self.db.set_series(id, series, notify=False) + num = next if do_autonumber and series else 1.0 + self.db.set_series_index(id, num, notify=False) + upd() + + if do_remove_format: + self.db.remove_format(id, remove_format, index_is_id=True, notify=False) + upd() + + + if do_remove_conv: + self.db.delete_conversion_options(id, 'PIPE') + upd() + + upd() + for w in getattr(self, 'custom_column_widgets', []): + w.commit(self.ids) + self.db.bulk_modify_tags(self.ids, add=add, remove=remove, + notify=False) + upd() + + + finally: + pd.cancel() + + return QDialog.accept(self) def series_changed(self): From 817bde27aa5151a8b8c0f1620b111c2e76252fe5 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 17 Aug 2010 00:03:01 -0600 Subject: [PATCH 35/73] ... --- src/calibre/gui2/dialogs/metadata_bulk.py | 13 +++++-------- src/calibre/gui2/dialogs/progress.py | 14 ++++++++++++-- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index dac3e3f477..29ba22a5ac 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -4,13 +4,14 @@ __copyright__ = '2008, Kovid Goyal ' '''Dialog to edit metadata in bulk''' from PyQt4.Qt import SIGNAL, QObject, QDialog, QGridLayout, \ - QProgressDialog, QCoreApplication, QString + QCoreApplication from calibre.gui2.dialogs.metadata_bulk_ui import Ui_MetadataBulkDialog from calibre.gui2.dialogs.tag_editor import TagEditor from calibre.ebooks.metadata import string_to_authors, \ authors_to_string from calibre.gui2.custom_column_widgets import populate_metadata_page +from calibre.gui2.dialogs.progress import ProgressDialog class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): @@ -106,12 +107,11 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): if len(self.ids) < 1: return QDialog.accept(self) - pd = QProgressDialog( + pd = ProgressDialog(_('Working'), _('Applying changes to %d books. This may take a while.')%len(self.ids), - QString(), 0, 0, self) + 0, 0, self, cancelable=False) pd.setModal(True) pd.show() - pd.setValue(0) def upd(): QCoreApplication.processEvents() @@ -164,7 +164,6 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): if rating != -1: self.db.set_rating(id, 2*rating, notify=False) - upd() if pub: self.db.set_publisher(id, pub, notify=False) @@ -181,10 +180,8 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): self.db.remove_format(id, remove_format, index_is_id=True, notify=False) upd() - if do_remove_conv: self.db.delete_conversion_options(id, 'PIPE') - upd() upd() for w in getattr(self, 'custom_column_widgets', []): @@ -195,7 +192,7 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): finally: - pd.cancel() + pd.hide() return QDialog.accept(self) diff --git a/src/calibre/gui2/dialogs/progress.py b/src/calibre/gui2/dialogs/progress.py index 40404050ec..3afa8dd633 100644 --- a/src/calibre/gui2/dialogs/progress.py +++ b/src/calibre/gui2/dialogs/progress.py @@ -13,7 +13,8 @@ class ProgressDialog(QDialog, Ui_Dialog): canceled_signal = pyqtSignal() - def __init__(self, title, msg='', min=0, max=99, parent=None): + def __init__(self, title, msg='', min=0, max=99, parent=None, + cancelable=True): QDialog.__init__(self, parent) self.setupUi(self) self.setWindowTitle(title) @@ -26,6 +27,9 @@ class ProgressDialog(QDialog, Ui_Dialog): self.canceled = False self.button_box.rejected.connect(self._canceled) + if not cancelable: + self.button_box.setVisible(False) + self.cancelable = cancelable def set_msg(self, msg=''): self.message.setText(msg) @@ -54,8 +58,14 @@ class ProgressDialog(QDialog, Ui_Dialog): self.title.setText(_('Aborting...')) self.canceled_signal.emit() + def reject(self): + if not self.cancelable: + return + QDialog.reject(self) + def keyPressEvent(self, ev): if ev.key() == Qt.Key_Escape: - self._canceled() + if self.cancelable: + self._canceled() else: QDialog.keyPressEvent(self, ev) From 83fc6c54f68ad2b8dfd0e4cd4521f25058672ffb Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 17 Aug 2010 10:30:32 +0100 Subject: [PATCH 36/73] Fix problems with restrictions when changing libraries: - restrictions box is not reloaded - counts are not correct Add tweak to set restriction at calibre startup --- resources/default_tweaks.py | 5 +++++ src/calibre/gui2/search_box.py | 16 +++------------- src/calibre/gui2/search_restriction_mixin.py | 10 ++++++++++ src/calibre/gui2/ui.py | 10 ++++++++-- 4 files changed, 26 insertions(+), 15 deletions(-) diff --git a/resources/default_tweaks.py b/resources/default_tweaks.py index 80456234b9..fb7b6cb0de 100644 --- a/resources/default_tweaks.py +++ b/resources/default_tweaks.py @@ -80,3 +80,8 @@ title_series_sorting = 'library_order' # strictly_alphabetic, it would remain "The Client". save_template_title_series_sorting = 'library_order' +# Specify a restriction to apply when calibre starts or when change library is +# used. Provide the name of a saved search. It is ignored if the saved search +# does not exist in the library being opened. The value '' means no restriction. +restrict_at_startup = '' + diff --git a/src/calibre/gui2/search_box.py b/src/calibre/gui2/search_box.py index 562292c2f6..95667379a1 100644 --- a/src/calibre/gui2/search_box.py +++ b/src/calibre/gui2/search_box.py @@ -382,8 +382,7 @@ class SearchBoxMixin(object): class SavedSearchBoxMixin(object): - def __init__(self, db): - self.db = db + def __init__(self): self.connect(self.saved_search, SIGNAL('changed()'), self.saved_searches_changed) self.saved_searches_changed() self.connect(self.clear_button, SIGNAL('clicked()'), self.saved_search.clear_to_help) @@ -402,10 +401,6 @@ class SavedSearchBoxMixin(object): b = getattr(self, x+'_search_button') b.setStatusTip(b.toolTip()) - def set_database(self, db): - self.db = db - self.saved_searches_changed() - def saved_searches_changed(self): p = saved_searches().names() p.sort() @@ -415,13 +410,8 @@ class SavedSearchBoxMixin(object): self.tags_view.recount() for s in p: self.search_restriction.addItem(s) - if t: - if t in p: # redo the current restriction, if there was one - self.search_restriction.setCurrentIndex(self.search_restriction.findText(t)) - # self.tags_view.set_search_restriction(t) - else: - self.search_restriction.setCurrentIndex(0) - self.apply_search_restriction('') + if t: # redo the search restriction if there was one + self.apply_named_search_restriction(t) def do_saved_search_edit(self, search): d = SavedSearchEditor(self, search) diff --git a/src/calibre/gui2/search_restriction_mixin.py b/src/calibre/gui2/search_restriction_mixin.py index a4186ad8d1..139d7c551d 100644 --- a/src/calibre/gui2/search_restriction_mixin.py +++ b/src/calibre/gui2/search_restriction_mixin.py @@ -29,6 +29,16 @@ class SearchRestrictionMixin(object): if self.restriction_in_effect: self.set_number_of_books_shown() + def apply_named_search_restriction(self, name): + if not name: + r = 0 + else: + r = self.search_restriction.findText(name) + if r < 0: + r = 0 + self.search_restriction.setCurrentIndex(r) + self.apply_search_restriction(r) + def apply_search_restriction(self, i): r = unicode(self.search_restriction.currentText()) if r is not None and r != '': diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 95cccb72a5..e87d39ba55 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -21,7 +21,7 @@ from PyQt4.Qt import Qt, SIGNAL, QTimer, \ from calibre import prints from calibre.constants import __appname__, isosx from calibre.ptempfile import PersistentTemporaryFile -from calibre.utils.config import prefs, dynamic +from calibre.utils.config import prefs, dynamic, tweaks from calibre.utils.ipc.server import Server from calibre.library.database2 import LibraryDatabase2 from calibre.customize.ui import interface_actions @@ -195,7 +195,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{ UpdateMixin.__init__(self, opts) ####################### Search boxes ######################## - SavedSearchBoxMixin.__init__(self, db) + SavedSearchBoxMixin.__init__(self) SearchBoxMixin.__init__(self) ####################### Library view ######################## @@ -230,6 +230,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{ ######################### Search Restriction ########################## SearchRestrictionMixin.__init__(self) + if tweaks['restrict_at_startup']: + self.apply_named_search_restriction(tweaks['restrict_at_startup']) ########################### Cover Flow ################################ @@ -371,6 +373,10 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{ for action in self.iactions.values(): action.library_changed(db) self.set_window_title() + self.apply_named_search_restriction('') # reset restriction to null + self.saved_searches_changed() # reload the search restrictions combo box + if tweaks['restrict_at_startup']: + self.apply_named_search_restriction(tweaks['restrict_at_startup']) def set_window_title(self): self.setWindowTitle(__appname__ + u' - ||%s||'%self.iactions['Choose Library'].library_name()) From fa68736f6ae77b1b3ecf993bfccf94af177ae674 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 17 Aug 2010 12:31:34 +0100 Subject: [PATCH 37/73] Port bulk tags update to custom columns --- src/calibre/gui2/custom_column_widgets.py | 55 ++++++++------- src/calibre/library/custom_columns.py | 81 +++++++++++++++++++++++ 2 files changed, 112 insertions(+), 24 deletions(-) diff --git a/src/calibre/gui2/custom_column_widgets.py b/src/calibre/gui2/custom_column_widgets.py index 3ed7d0c4ad..d624d5320d 100644 --- a/src/calibre/gui2/custom_column_widgets.py +++ b/src/calibre/gui2/custom_column_widgets.py @@ -394,30 +394,18 @@ class BulkBase(Base): ans = list(ans) return ans - def process_each_book(self): - return False - def initialize(self, book_ids): - if not self.process_each_book(): - self.initial_val = val = self.get_initial_value(book_ids) - val = self.normalize_db_val(val) - self.setter(val) + self.initial_val = val = self.get_initial_value(book_ids) + val = self.normalize_db_val(val) + self.setter(val) def commit(self, book_ids, notify=False): - if self.process_each_book(): + val = self.getter() + val = self.normalize_ui_val(val) + if val != self.initial_val: for book_id in book_ids: QCoreApplication.processEvents() - val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True) - new_val = self.getter(val) - if set(val) != new_val: - self.db.set_custom(book_id, new_val, num=self.col_id, notify=notify) - else: - val = self.getter() - val = self.normalize_ui_val(val) - if val != self.initial_val: - for book_id in book_ids: - QCoreApplication.processEvents() - self.db.set_custom(book_id, val, num=self.col_id, notify=notify) + self.db.set_custom(book_id, val, num=self.col_id, notify=notify) class BulkBool(BulkBase, Bool): pass @@ -474,9 +462,6 @@ class BulkSeries(BulkBase): self.db.set_custom(book_id, val, extra=s_index, num=self.col_id, notify=notify) - def process_each_book(self): - return True - class RemoveTags(QWidget): def __init__(self, parent, values): @@ -539,8 +524,30 @@ class BulkText(BulkBase): if idx is not None: self.widgets[1].setCurrentIndex(idx) - def process_each_book(self): - return self.col_metadata['is_multiple'] + def commit(self, book_ids, notify=False): + if self.col_metadata['is_multiple']: + remove = set() + if self.removing_widget.checkbox.isChecked(): + for book_id in book_ids: + remove |= set(self.db.get_custom(book_id, num=self.col_id, + index_is_id=True)) + else: + txt = unicode(self.removing_widget.tags_box.text()) + if txt: + remove = set([v.strip() for v in txt.split(',')]) + txt = unicode(self.adding_widget.text()) + if txt: + add = set([v.strip() for v in txt.split(',')]) + else: + add = set() + self.db.set_custom_bulk(book_ids, add=add, remove=remove, num=self.col_id) + else: + val = self.getter() + val = self.normalize_ui_val(val) + if val != self.initial_val: + for book_id in book_ids: + QCoreApplication.processEvents() + self.db.set_custom(book_id, val, num=self.col_id, notify=notify) def getter(self, original_value = None): if self.col_metadata['is_multiple']: diff --git a/src/calibre/library/custom_columns.py b/src/calibre/library/custom_columns.py index b8e0f8d3b6..3b4a84af4f 100644 --- a/src/calibre/library/custom_columns.py +++ b/src/calibre/library/custom_columns.py @@ -313,6 +313,87 @@ class CustomColumns(object): self.conn.commit() return changed + def set_custom_bulk(self, ids, add=[], remove=[], + label=None, num=None, notify=False): + if label is not None: + data = self.custom_column_label_map[label] + if num is not None: + data = self.custom_column_num_map[num] + if not data['editable']: + raise ValueError('Column %r is not editable'%data['label']) + if data['datatype'] != 'text' or not data['is_multiple']: + raise ValueError('Column %r is not text/multiple'%data['label']) + + add = self.cleanup_tags(add) + remove = self.cleanup_tags(remove) + remove = set(remove) - set(add) + if not ids or (not add and not remove): + return + # get custom table names + cust_table, link_table = self.custom_table_names(data['num']) + + # Add tags that do not already exist into the custom cust_table + all_tags = self.all_custom(num=data['num']) + lt = [t.lower() for t in all_tags] + new_tags = [t for t in add if t.lower() not in lt] + if new_tags: + self.conn.executemany('INSERT INTO %s(value) VALUES (?)'%cust_table, + [(x,) for x in new_tags]) + + # Create the temporary temp_tables to store the ids for books and tags + # to be operated on + temp_tables = ('temp_bulk_tag_edit_books', 'temp_bulk_tag_edit_add', + 'temp_bulk_tag_edit_remove') + drops = '\n'.join(['DROP TABLE IF EXISTS %s;'%t for t in temp_tables]) + creates = '\n'.join(['CREATE TEMP TABLE %s(id INTEGER PRIMARY KEY);'%t + for t in temp_tables]) + self.conn.executescript(drops + creates) + + # Populate the books temp cust_table + self.conn.executemany( + 'INSERT INTO temp_bulk_tag_edit_books VALUES (?)', + [(x,) for x in ids]) + + # Populate the add/remove tags temp temp_tables + for table, tags in enumerate([add, remove]): + if not tags: + continue + table = temp_tables[table+1] + insert = ('INSERT INTO {tt}(id) SELECT {ct}.id FROM {ct} WHERE value=?' + ' COLLATE PYNOCASE LIMIT 1').format(tt=table, ct=cust_table) + self.conn.executemany(insert, [(x,) for x in tags]) + + # now do the real work -- removing and adding the tags + if remove: + self.conn.execute( + '''DELETE FROM %s WHERE + book IN (SELECT id FROM %s) AND + value IN (SELECT id FROM %s)''' + % (link_table, temp_tables[0], temp_tables[2])) + if add: + self.conn.execute( + ''' + INSERT INTO {0}(book, value) SELECT {1}.id, {2}.id FROM {1}, {2} + '''.format(link_table, temp_tables[0], temp_tables[1]) + ) + # get rid of the temp tables + self.conn.executescript(drops) + # Remove any dreg tags -- ones with no references + self.conn.execute( + '''DELETE FROM %s WHERE (SELECT COUNT(id) FROM %s WHERE + value=%s.id) < 1''' % (cust_table, link_table, cust_table)) + self.conn.commit() + + # set the in-memory copies of the tags + for x in ids: + tags = self.conn.get( + 'SELECT custom_%s FROM meta2 WHERE id=?'%data['num'], + (x,), all=False) + self.data.set(x, self.FIELD_MAP[data['num']], tags, row_is_id=True) + + if notify: + self.notify('metadata', ids) + def set_custom(self, id_, val, label=None, num=None, append=False, notify=True, extra=None): if label is not None: From 190428b66dfc268f534cae75dc835c4f6b6f7cfc Mon Sep 17 00:00:00 2001 From: GRiker Date: Tue, 17 Aug 2010 04:35:08 -0700 Subject: [PATCH 38/73] GwR fix for genre tag regex --- src/calibre/gui2/catalog/catalog_epub_mobi.py | 2 +- src/calibre/gui2/catalog/catalog_epub_mobi.ui | 2 +- src/calibre/library/catalog.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.py b/src/calibre/gui2/catalog/catalog_epub_mobi.py index 6ecab3c081..5acda0948c 100644 --- a/src/calibre/gui2/catalog/catalog_epub_mobi.py +++ b/src/calibre/gui2/catalog/catalog_epub_mobi.py @@ -16,7 +16,7 @@ class PluginWidget(QWidget,Ui_Form): TITLE = _('E-book options') HELP = _('Options specific to')+' EPUB/MOBI '+_('output') - OPTION_FIELDS = [('exclude_genre','\[[\w ]*\]'), + OPTION_FIELDS = [('exclude_genre','\[.+\]'), ('exclude_tags','~,'+_('Catalog')), ('generate_titles', True), ('generate_recently_added', True), diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.ui b/src/calibre/gui2/catalog/catalog_epub_mobi.ui index dab8c972c7..cdf91eed6f 100644 --- a/src/calibre/gui2/catalog/catalog_epub_mobi.ui +++ b/src/calibre/gui2/catalog/catalog_epub_mobi.ui @@ -80,7 +80,7 @@ Regex tips: -- The default regex - \[[\w ]*\] - excludes genre tags of the form [tag], e.g., [Amazon Freebie] +- The default regex - \[.+\] - excludes genre tags of the form [tag], e.g., [Amazon Freebie] - A regex pattern of a single dot excludes all genre tags, generating no Genre Section diff --git a/src/calibre/library/catalog.py b/src/calibre/library/catalog.py index ed41ecb76e..b4fd537729 100644 --- a/src/calibre/library/catalog.py +++ b/src/calibre/library/catalog.py @@ -541,7 +541,7 @@ class EPUB_MOBI(CatalogPlugin): "Default: '%default'None\n" "Applies to: ePub, MOBI output formats")), Option('--exclude-genre', - default='\[[\w ]*\]', + default='\[.+\]', dest='exclude_genre', action = None, help=_("Regex describing tags to exclude as genres.\n" "Default: '%default' excludes bracketed tags, e.g. '[]'\n" From 35b0faf843ea9f58e50596bc730c4fcdb5019dee Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 17 Aug 2010 13:31:31 +0100 Subject: [PATCH 39/73] Add a restriction argument to the content server command line. Includes changes to make the restriction work. --- src/calibre/library/server/base.py | 4 +++- src/calibre/library/server/cache.py | 2 +- src/calibre/library/server/main.py | 6 +++++- src/calibre/library/server/mobile.py | 3 ++- src/calibre/library/server/xml.py | 3 ++- 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/calibre/library/server/base.py b/src/calibre/library/server/base.py index 0097276348..57e5e702fa 100644 --- a/src/calibre/library/server/base.py +++ b/src/calibre/library/server/base.py @@ -57,13 +57,15 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache): server_name = __appname__ + '/' + __version__ - def __init__(self, db, opts, embedded=False, show_tracebacks=True): + def __init__(self, db, opts, embedded=False, show_tracebacks=True, + ignore_search_restriction=True): self.db = db for item in self.db: item break self.opts = opts self.embedded = embedded + self.ignore_search_restriction=ignore_search_restriction self.state_callback = None self.max_cover_width, self.max_cover_height = \ map(int, self.opts.max_cover.split('x')) diff --git a/src/calibre/library/server/cache.py b/src/calibre/library/server/cache.py index 9fec2c2737..6f6c21e60c 100644 --- a/src/calibre/library/server/cache.py +++ b/src/calibre/library/server/cache.py @@ -18,7 +18,7 @@ class Cache(object): old = self._search_cache.pop(search, None) if old is None or old[0] <= self.db.last_modified(): matches = self.db.data.search(search, return_matches=True, - ignore_search_restriction=True) + ignore_search_restriction=self.ignore_search_restriction) if not matches: matches = [] self._search_cache[search] = (utcnow(), frozenset(matches)) diff --git a/src/calibre/library/server/main.py b/src/calibre/library/server/main.py index 5ca82c6b98..2fad001a86 100644 --- a/src/calibre/library/server/main.py +++ b/src/calibre/library/server/main.py @@ -32,6 +32,8 @@ def option_parser(): help=_('Write process PID to the specified file')) parser.add_option('--daemonize', default=False, action='store_true', help='Run process in background as a daemon. No effect on windows.') + parser.add_option('--restriction', default=None, + help='Specifies a restriction to be used for this invocation.') return parser def daemonize(stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'): @@ -83,7 +85,9 @@ def main(args=sys.argv): if opts.with_library is None: opts.with_library = prefs['library_path'] db = LibraryDatabase2(opts.with_library) - server = LibraryServer(db, opts) + server = LibraryServer(db, opts, ignore_search_restriction=False) + if opts.restriction: + db.data.set_search_restriction('search:' + opts.restriction) server.start() return 0 diff --git a/src/calibre/library/server/mobile.py b/src/calibre/library/server/mobile.py index c3667a2077..391ad70bfd 100644 --- a/src/calibre/library/server/mobile.py +++ b/src/calibre/library/server/mobile.py @@ -181,7 +181,8 @@ class MobileServer(object): num = int(num) except ValueError: raise cherrypy.HTTPError(400, 'num: %s is not an integer'%num) - ids = self.db.data.parse(search) if search and search.strip() else self.db.data.universal_set() + ids = self.db.search(search, return_matches=True, + ignore_search_restriction=self.ignore_search_restriction) FM = self.db.FIELD_MAP items = [r for r in iter(self.db) if r[FM['id']] in ids] if sort is not None: diff --git a/src/calibre/library/server/xml.py b/src/calibre/library/server/xml.py index 036a2051bf..5649208036 100644 --- a/src/calibre/library/server/xml.py +++ b/src/calibre/library/server/xml.py @@ -45,7 +45,8 @@ class XMLServer(object): order = order.lower().strip() == 'ascending' - ids = self.db.data.parse(search) if search and search.strip() else self.db.data.universal_set() + ids = self.db.search(search, return_matches=True, + ignore_search_restriction=self.ignore_search_restriction) FM = self.db.FIELD_MAP From d9f60b415ff1fceaa5c4276cd55638ecac666ce2 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 17 Aug 2010 10:38:55 -0600 Subject: [PATCH 40/73] Do not allow the user to override the default tweaks or the hyphenate javascript. Also if a file is not found, do not use the user location as the default base. Fixes #6524 (Problem opening Preferences in Ebook-Viewer v7.14) --- src/calibre/gui2/viewer/documentview.py | 3 ++- src/calibre/utils/config.py | 3 ++- src/calibre/utils/resources.py | 17 ++++++++++++----- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/calibre/gui2/viewer/documentview.py b/src/calibre/gui2/viewer/documentview.py index 4653529095..75f95b1a90 100644 --- a/src/calibre/gui2/viewer/documentview.py +++ b/src/calibre/gui2/viewer/documentview.py @@ -81,7 +81,8 @@ class ConfigDialog(QDialog, Ui_Dialog): self.css.setToolTip(_('Set the user CSS stylesheet. This can be used to customize the look of all books.')) self.max_view_width.setValue(opts.max_view_width) pats = [os.path.basename(x).split('.')[0] for x in - glob.glob(P('viewer/hyphenate/patterns/*.js'))] + glob.glob(P('viewer/hyphenate/patterns/*.js', + allow_user_override=False))] names = list(map(get_language, pats)) pmap = {} for i in range(len(pats)): diff --git a/src/calibre/utils/config.py b/src/calibre/utils/config.py index 3d2663cd1d..5c8fe523e3 100644 --- a/src/calibre/utils/config.py +++ b/src/calibre/utils/config.py @@ -705,7 +705,8 @@ if prefs['installation_uuid'] is None: # Read tweaks def read_raw_tweaks(): make_config_dir() - default_tweaks = P('default_tweaks.py', data=True) + default_tweaks = P('default_tweaks.py', data=True, + allow_user_override=False) tweaks_file = os.path.join(config_dir, 'tweaks.py') if not os.path.exists(tweaks_file): with open(tweaks_file, 'wb') as f: diff --git a/src/calibre/utils/resources.py b/src/calibre/utils/resources.py index dd600eb627..97c14926e4 100644 --- a/src/calibre/utils/resources.py +++ b/src/calibre/utils/resources.py @@ -25,29 +25,36 @@ class PathResolver(object): pass return False + self.default_path = sys.resources_location + dev_path = os.environ.get('CALIBRE_DEVELOP_FROM', None) if dev_path is not None: dev_path = os.path.join(os.path.abspath( os.path.dirname(dev_path)), 'resources') if suitable(dev_path): self.locations.insert(0, dev_path) + self.default_path = dev_path user_path = os.path.join(config_dir, 'resources') + self.user_path = None if suitable(user_path): self.locations.insert(0, user_path) + self.user_path = user_path - def __call__(self, path): + def __call__(self, path, allow_user_override=True): path = path.replace(os.sep, '/') ans = self.cache.get(path, None) if ans is None: for base in self.locations: + if not allow_user_override and base == self.user_path: + continue fpath = os.path.join(base, *path.split('/')) if os.path.exists(fpath): ans = fpath break if ans is None: - ans = os.path.join(self.locations[0], *path.split('/')) + ans = os.path.join(self.default_path, *path.split('/')) self.cache[path] = ans @@ -55,13 +62,13 @@ class PathResolver(object): _resolver = PathResolver() -def get_path(path, data=False): - fpath = _resolver(path) +def get_path(path, data=False, allow_user_override=True): + fpath = _resolver(path, allow_user_override=allow_user_override) if data: return open(fpath, 'rb').read() return fpath -def get_image_path(path, data=False): +def get_image_path(path, data=False, allow_user_override=True): return get_path('images/'+path, data=data) __builtin__.__dict__['P'] = get_path From 2fc237c7dedcdb4ebab5fc90b1fc4269fcc7f4c2 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 17 Aug 2010 12:00:56 -0600 Subject: [PATCH 41/73] Remove orphans after bulk metadata edit --- src/calibre/gui2/dialogs/metadata_bulk.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index 29ba22a5ac..0139d0aee2 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -194,6 +194,7 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): finally: pd.hide() + self.db.clean() return QDialog.accept(self) From 7a68c4001bb901f5d41791c21907b4ead1730b43 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 17 Aug 2010 12:15:45 -0600 Subject: [PATCH 42/73] Faster bulk update for custom column is_multiple datatypes --- src/calibre/gui2/custom_column_widgets.py | 55 ++++++++------- src/calibre/library/custom_columns.py | 81 +++++++++++++++++++++++ 2 files changed, 112 insertions(+), 24 deletions(-) diff --git a/src/calibre/gui2/custom_column_widgets.py b/src/calibre/gui2/custom_column_widgets.py index 3ed7d0c4ad..d624d5320d 100644 --- a/src/calibre/gui2/custom_column_widgets.py +++ b/src/calibre/gui2/custom_column_widgets.py @@ -394,30 +394,18 @@ class BulkBase(Base): ans = list(ans) return ans - def process_each_book(self): - return False - def initialize(self, book_ids): - if not self.process_each_book(): - self.initial_val = val = self.get_initial_value(book_ids) - val = self.normalize_db_val(val) - self.setter(val) + self.initial_val = val = self.get_initial_value(book_ids) + val = self.normalize_db_val(val) + self.setter(val) def commit(self, book_ids, notify=False): - if self.process_each_book(): + val = self.getter() + val = self.normalize_ui_val(val) + if val != self.initial_val: for book_id in book_ids: QCoreApplication.processEvents() - val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True) - new_val = self.getter(val) - if set(val) != new_val: - self.db.set_custom(book_id, new_val, num=self.col_id, notify=notify) - else: - val = self.getter() - val = self.normalize_ui_val(val) - if val != self.initial_val: - for book_id in book_ids: - QCoreApplication.processEvents() - self.db.set_custom(book_id, val, num=self.col_id, notify=notify) + self.db.set_custom(book_id, val, num=self.col_id, notify=notify) class BulkBool(BulkBase, Bool): pass @@ -474,9 +462,6 @@ class BulkSeries(BulkBase): self.db.set_custom(book_id, val, extra=s_index, num=self.col_id, notify=notify) - def process_each_book(self): - return True - class RemoveTags(QWidget): def __init__(self, parent, values): @@ -539,8 +524,30 @@ class BulkText(BulkBase): if idx is not None: self.widgets[1].setCurrentIndex(idx) - def process_each_book(self): - return self.col_metadata['is_multiple'] + def commit(self, book_ids, notify=False): + if self.col_metadata['is_multiple']: + remove = set() + if self.removing_widget.checkbox.isChecked(): + for book_id in book_ids: + remove |= set(self.db.get_custom(book_id, num=self.col_id, + index_is_id=True)) + else: + txt = unicode(self.removing_widget.tags_box.text()) + if txt: + remove = set([v.strip() for v in txt.split(',')]) + txt = unicode(self.adding_widget.text()) + if txt: + add = set([v.strip() for v in txt.split(',')]) + else: + add = set() + self.db.set_custom_bulk(book_ids, add=add, remove=remove, num=self.col_id) + else: + val = self.getter() + val = self.normalize_ui_val(val) + if val != self.initial_val: + for book_id in book_ids: + QCoreApplication.processEvents() + self.db.set_custom(book_id, val, num=self.col_id, notify=notify) def getter(self, original_value = None): if self.col_metadata['is_multiple']: diff --git a/src/calibre/library/custom_columns.py b/src/calibre/library/custom_columns.py index b8e0f8d3b6..7c613295b9 100644 --- a/src/calibre/library/custom_columns.py +++ b/src/calibre/library/custom_columns.py @@ -313,6 +313,87 @@ class CustomColumns(object): self.conn.commit() return changed + def set_custom_bulk(self, ids, add=[], remove=[], + label=None, num=None, notify=False): + ''' + Fast algorithm for updating custom column is_multiple datatypes. + Do not use with other custom column datatypes. + ''' + if label is not None: + data = self.custom_column_label_map[label] + if num is not None: + data = self.custom_column_num_map[num] + if not data['editable']: + raise ValueError('Column %r is not editable'%data['label']) + if data['datatype'] != 'text' or not data['is_multiple']: + raise ValueError('Column %r is not text/multiple'%data['label']) + + add = self.cleanup_tags(add) + remove = self.cleanup_tags(remove) + remove = set(remove) - set(add) + if not ids or (not add and not remove): + return + # get custom table names + cust_table, link_table = self.custom_table_names(data['num']) + + # Add tags that do not already exist into the custom cust_table + all_tags = self.all_custom(num=data['num']) + lt = [t.lower() for t in all_tags] + new_tags = [t for t in add if t.lower() not in lt] + if new_tags: + self.conn.executemany('INSERT INTO %s(value) VALUES (?)'%cust_table, + [(x,) for x in new_tags]) + + # Create the temporary temp_tables to store the ids for books and tags + # to be operated on + temp_tables = ('temp_bulk_tag_edit_books', 'temp_bulk_tag_edit_add', + 'temp_bulk_tag_edit_remove') + drops = '\n'.join(['DROP TABLE IF EXISTS %s;'%t for t in temp_tables]) + creates = '\n'.join(['CREATE TEMP TABLE %s(id INTEGER PRIMARY KEY);'%t + for t in temp_tables]) + self.conn.executescript(drops + creates) + + # Populate the books temp cust_table + self.conn.executemany( + 'INSERT INTO temp_bulk_tag_edit_books VALUES (?)', + [(x,) for x in ids]) + + # Populate the add/remove tags temp temp_tables + for table, tags in enumerate([add, remove]): + if not tags: + continue + table = temp_tables[table+1] + insert = ('INSERT INTO {tt}(id) SELECT {ct}.id FROM {ct} WHERE value=?' + ' COLLATE PYNOCASE LIMIT 1').format(tt=table, ct=cust_table) + self.conn.executemany(insert, [(x,) for x in tags]) + + # now do the real work -- removing and adding the tags + if remove: + self.conn.execute( + '''DELETE FROM %s WHERE + book IN (SELECT id FROM %s) AND + value IN (SELECT id FROM %s)''' + % (link_table, temp_tables[0], temp_tables[2])) + if add: + self.conn.execute( + ''' + INSERT INTO {0}(book, value) SELECT {1}.id, {2}.id FROM {1}, {2} + '''.format(link_table, temp_tables[0], temp_tables[1]) + ) + # get rid of the temp tables + self.conn.executescript(drops) + self.conn.commit() + + # set the in-memory copies of the tags + for x in ids: + tags = self.conn.get( + 'SELECT custom_%s FROM meta2 WHERE id=?'%data['num'], + (x,), all=False) + self.data.set(x, self.FIELD_MAP[data['num']], tags, row_is_id=True) + + if notify: + self.notify('metadata', ids) + def set_custom(self, id_, val, label=None, num=None, append=False, notify=True, extra=None): if label is not None: From 61dd7b9e9ada322270a9c55c8d878051cb218568 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 17 Aug 2010 21:14:27 +0100 Subject: [PATCH 43/73] Add restriction to content server --- src/calibre/gui2/dialogs/config/__init__.py | 2 ++ src/calibre/gui2/dialogs/config/config.ui | 20 +++++++++++++ src/calibre/library/caches.py | 32 ++++++++++----------- src/calibre/library/database2.py | 1 + src/calibre/library/server/base.py | 14 +++++++-- src/calibre/library/server/cache.py | 3 +- src/calibre/library/server/main.py | 7 ++--- src/calibre/library/server/mobile.py | 3 +- src/calibre/library/server/xml.py | 4 +-- 9 files changed, 55 insertions(+), 31 deletions(-) diff --git a/src/calibre/gui2/dialogs/config/__init__.py b/src/calibre/gui2/dialogs/config/__init__.py index 20c7843aa8..c5b61f146d 100644 --- a/src/calibre/gui2/dialogs/config/__init__.py +++ b/src/calibre/gui2/dialogs/config/__init__.py @@ -447,6 +447,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog): self.password.setText(opts.password if opts.password else '') self.opt_max_opds_items.setValue(opts.max_opds_items) self.opt_max_opds_ungrouped_items.setValue(opts.max_opds_ungrouped_items) + self.opt_restriction.setText(self.db.prefs.get('cs_restriction', '')) self.auto_launch.setChecked(config['autolaunch_server']) self.systray_icon.setChecked(config['systray_icon']) self.sync_news.setChecked(config['upload_news_to_device']) @@ -906,6 +907,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog): sc.set('max_opds_items', self.opt_max_opds_items.value()) sc.set('max_opds_ungrouped_items', self.opt_max_opds_ungrouped_items.value()) + self.db.prefs.set('cs_restriction', unicode(self.opt_restriction.text())) config['delete_news_from_library_on_upload'] = self.delete_news.isChecked() config['upload_news_to_device'] = self.sync_news.isChecked() config['search_as_you_type'] = self.search_as_you_type.isChecked() diff --git a/src/calibre/gui2/dialogs/config/config.ui b/src/calibre/gui2/dialogs/config/config.ui index 1359278512..62eb7bb620 100644 --- a/src/calibre/gui2/dialogs/config/config.ui +++ b/src/calibre/gui2/dialogs/config/config.ui @@ -1040,6 +1040,26 @@ + + + + Provides a restriction to be used by the content server + + + + + + + + + + Restriction (saved search) to apply: + + + opt_restriction + + + diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index fa07ed8b83..ca66d28ddb 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -609,26 +609,24 @@ class ResultCache(SearchQueryParser): self._map.sort(cmp=fcmp, reverse=not ascending) self._map_filtered = [id for id in self._map if id in self._map_filtered] - def search(self, query, return_matches=False, - ignore_search_restriction=False): - q = '' - if not query or not query.strip(): - if not ignore_search_restriction: - q = self.search_restriction - else: - q = query - if not ignore_search_restriction and self.search_restriction: - q = u'%s (%s)' % (self.search_restriction, query) - if not q: - if return_matches: - return list(self._map) # when return_matches, do not update the maps! - self._map_filtered = list(self._map) - return - matches = sorted(self.parse(q)) - ans = [id for id in self._map if id in matches] + def search(self, query, return_matches=False): + ans = self.search_getting_ids(query, self.search_restriction) if return_matches: return ans self._map_filtered = ans + def search_getting_ids(self, query, search_restriction): + q = '' + if not query or not query.strip(): + q = search_restriction + else: + q = query + if search_restriction: + q = u'%s (%s)' % (search_restriction, query) + if not q: + return list(self._map) + matches = sorted(self.parse(q)) + return [id for id in self._map if id in matches] + def set_search_restriction(self, s): self.search_restriction = s diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index b8ac065760..6728735eb3 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -296,6 +296,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.book_on_device_func = None self.data = ResultCache(self.FIELD_MAP, self.field_metadata) self.search = self.data.search + self.search_getting_ids = self.data.search_getting_ids self.refresh = functools.partial(self.data.refresh, self) self.sort = self.data.sort self.index = self.data.index diff --git a/src/calibre/library/server/base.py b/src/calibre/library/server/base.py index 57e5e702fa..bdaee1c42d 100644 --- a/src/calibre/library/server/base.py +++ b/src/calibre/library/server/base.py @@ -57,15 +57,13 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache): server_name = __appname__ + '/' + __version__ - def __init__(self, db, opts, embedded=False, show_tracebacks=True, - ignore_search_restriction=True): + def __init__(self, db, opts, embedded=False, show_tracebacks=True): self.db = db for item in self.db: item break self.opts = opts self.embedded = embedded - self.ignore_search_restriction=ignore_search_restriction self.state_callback = None self.max_cover_width, self.max_cover_height = \ map(int, self.opts.max_cover.split('x')) @@ -97,9 +95,19 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache): 'tools.digest_auth.users' : {opts.username.strip():opts.password.strip()}, } + self.set_search_restriction(db.prefs.get('cs_restriction', '')) + if opts.restriction is not None: + self.set_search_restriction(opts.restriction) + self.is_running = False self.exception = None + def set_search_restriction(self, restriction): + if restriction: + self.search_restriction = 'search:'+restriction + else: + self.search_restriction = '' + def setup_loggers(self): access_file = log_access_file error_file = log_error_file diff --git a/src/calibre/library/server/cache.py b/src/calibre/library/server/cache.py index 6f6c21e60c..94e4a1c041 100644 --- a/src/calibre/library/server/cache.py +++ b/src/calibre/library/server/cache.py @@ -17,8 +17,7 @@ class Cache(object): def search_cache(self, search): old = self._search_cache.pop(search, None) if old is None or old[0] <= self.db.last_modified(): - matches = self.db.data.search(search, return_matches=True, - ignore_search_restriction=self.ignore_search_restriction) + matches = self.db.data.search_getting_ids(search, self.search_restriction) if not matches: matches = [] self._search_cache[search] = (utcnow(), frozenset(matches)) diff --git a/src/calibre/library/server/main.py b/src/calibre/library/server/main.py index 2fad001a86..e87d2d75d7 100644 --- a/src/calibre/library/server/main.py +++ b/src/calibre/library/server/main.py @@ -33,7 +33,8 @@ def option_parser(): parser.add_option('--daemonize', default=False, action='store_true', help='Run process in background as a daemon. No effect on windows.') parser.add_option('--restriction', default=None, - help='Specifies a restriction to be used for this invocation.') + help=_('Specifies a restriction to be used for this invocation. ' + 'This option overrides the default set in the GUI')) return parser def daemonize(stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'): @@ -85,9 +86,7 @@ def main(args=sys.argv): if opts.with_library is None: opts.with_library = prefs['library_path'] db = LibraryDatabase2(opts.with_library) - server = LibraryServer(db, opts, ignore_search_restriction=False) - if opts.restriction: - db.data.set_search_restriction('search:' + opts.restriction) + server = LibraryServer(db, opts) server.start() return 0 diff --git a/src/calibre/library/server/mobile.py b/src/calibre/library/server/mobile.py index 391ad70bfd..8ea9a04515 100644 --- a/src/calibre/library/server/mobile.py +++ b/src/calibre/library/server/mobile.py @@ -181,8 +181,7 @@ class MobileServer(object): num = int(num) except ValueError: raise cherrypy.HTTPError(400, 'num: %s is not an integer'%num) - ids = self.db.search(search, return_matches=True, - ignore_search_restriction=self.ignore_search_restriction) + ids = self.db.search_getting_ids(search, self.search_restriction) FM = self.db.FIELD_MAP items = [r for r in iter(self.db) if r[FM['id']] in ids] if sort is not None: diff --git a/src/calibre/library/server/xml.py b/src/calibre/library/server/xml.py index 5649208036..18370ab641 100644 --- a/src/calibre/library/server/xml.py +++ b/src/calibre/library/server/xml.py @@ -45,8 +45,7 @@ class XMLServer(object): order = order.lower().strip() == 'ascending' - ids = self.db.search(search, return_matches=True, - ignore_search_restriction=self.ignore_search_restriction) + ids = self.db.search_getting_ids(search, self.search_restriction) FM = self.db.FIELD_MAP @@ -54,7 +53,6 @@ class XMLServer(object): if sort is not None: self.sort(items, sort, order) - books = [] def serialize(x): From 037a377a1cc23048842517493e5d5107d43b654a Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 17 Aug 2010 21:23:28 +0100 Subject: [PATCH 44/73] Remove search restriction tweak --- resources/default_tweaks.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/resources/default_tweaks.py b/resources/default_tweaks.py index fe117ce586..d6f134f724 100644 --- a/resources/default_tweaks.py +++ b/resources/default_tweaks.py @@ -81,8 +81,3 @@ title_series_sorting = 'library_order' # strictly_alphabetic, it would remain "The Client". save_template_title_series_sorting = 'library_order' -# Specify a restriction to apply when calibre starts or when change library is -# used. Provide the name of a saved search. It is ignored if the saved search -# does not exist in the library being opened. The value '' means no restriction. -restrict_at_startup = '' - From 73de2ae89126eb060edceb1e30e4584f94c615c3 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 17 Aug 2010 21:47:50 +0100 Subject: [PATCH 45/73] Remove empty categories from the opds version --- src/calibre/library/server/opds.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/calibre/library/server/opds.py b/src/calibre/library/server/opds.py index f32e5ad47a..e1cbb79599 100644 --- a/src/calibre/library/server/opds.py +++ b/src/calibre/library/server/opds.py @@ -550,6 +550,8 @@ class OPDSServer(object): (_('Title'), _('Title'), 'Otitle'), ] for category in categories: + if len(categories[category]) == 0: + continue if category == 'formats': continue meta = category_meta.get(category, None) From 769248295583cbb19c55f67f195b63e27b077926 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 17 Aug 2010 16:16:22 -0600 Subject: [PATCH 46/73] Do not allow library to be changed when device is connected/jobs are running --- src/calibre/gui2/actions/choose_library.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/calibre/gui2/actions/choose_library.py b/src/calibre/gui2/actions/choose_library.py index 7bdbcf748a..29e508aff2 100644 --- a/src/calibre/gui2/actions/choose_library.py +++ b/src/calibre/gui2/actions/choose_library.py @@ -137,6 +137,8 @@ class ChooseLibraryAction(InterfaceAction): self.qaction.setEnabled(enabled) def switch_requested(self, location): + if not self.change_library_allowed(): + return loc = location.replace('/', os.sep) exists = self.gui.library_view.model().db.exists_at(loc) if not exists: @@ -164,9 +166,23 @@ class ChooseLibraryAction(InterfaceAction): a.setWhatsThis(tooltip) def choose_library(self, *args): + if not self.change_library_allowed(): + return from calibre.gui2.dialogs.choose_library import ChooseLibrary db = self.gui.library_view.model().db c = ChooseLibrary(db, self.gui.library_moved, self.gui) c.exec_() + def change_library_allowed(self): + if self.gui.device_connected: + warning_dialog(self.gui, _('Not allowed'), + _('You cannot change libraries when a device is' + ' connected.'), show=True) + return False + if self.gui.job_manager.has_jobs(): + warning_dialog(self.gui, _('Not allowed'), + _('You cannot change libraries while jobs' + ' are running.'), show=True) + return False + return True From 47fa5382b622304ea85cb7d87d9515d78815a7fe Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 17 Aug 2010 16:55:52 -0600 Subject: [PATCH 47/73] Fix #6540 (Requesting metadata download while one is already ongoing causes pop-up spam) --- src/calibre/gui2/actions/edit_metadata.py | 8 +- src/calibre/gui2/dialogs/progress.py | 37 ++- .../progress_indicator/QProgressIndicator.cpp | 248 +++++++++--------- .../progress_indicator/QProgressIndicator.h | 186 ++++++------- 4 files changed, 258 insertions(+), 221 deletions(-) diff --git a/src/calibre/gui2/actions/edit_metadata.py b/src/calibre/gui2/actions/edit_metadata.py index 9d1066ee40..bb07cfda3d 100644 --- a/src/calibre/gui2/actions/edit_metadata.py +++ b/src/calibre/gui2/actions/edit_metadata.py @@ -16,6 +16,7 @@ from calibre.gui2.dialogs.metadata_bulk import MetadataBulkDialog from calibre.gui2.dialogs.confirm_delete import confirm from calibre.gui2.dialogs.tag_list_editor import TagListEditor from calibre.gui2.actions import InterfaceAction +from calibre.gui2.dialogs.progress import BlockingBusy class EditMetadataAction(InterfaceAction): @@ -95,18 +96,19 @@ class EditMetadataAction(InterfaceAction): x = _('social metadata') else: x = _('covers') if covers and not set_metadata else _('metadata') - self.gui.progress_indicator.start( - _('Downloading %s for %d book(s)')%(x, len(ids))) self._book_metadata_download_check = QTimer(self.gui) self._book_metadata_download_check.timeout.connect(self.book_metadata_download_check, type=Qt.QueuedConnection) self._book_metadata_download_check.start(100) + self._bb_dialog = BlockingBusy(_('Downloading %s for %d book(s)')%(x, + len(ids)), parent=self.gui) + self._bb_dialog.exec_() def book_metadata_download_check(self): if self._download_book_metadata.is_alive(): return self._book_metadata_download_check.stop() - self.gui.progress_indicator.stop() + self._bb_dialog.accept() cr = self.gui.library_view.currentIndex().row() x = self._download_book_metadata self._download_book_metadata = None diff --git a/src/calibre/gui2/dialogs/progress.py b/src/calibre/gui2/dialogs/progress.py index 3afa8dd633..553ee4b03b 100644 --- a/src/calibre/gui2/dialogs/progress.py +++ b/src/calibre/gui2/dialogs/progress.py @@ -5,9 +5,10 @@ __docformat__ = 'restructuredtext en' '''''' -from PyQt4.Qt import QDialog, pyqtSignal, Qt +from PyQt4.Qt import QDialog, pyqtSignal, Qt, QVBoxLayout, QLabel, QFont from calibre.gui2.dialogs.progress_ui import Ui_Dialog +from calibre.gui2.progress_indicator import ProgressIndicator class ProgressDialog(QDialog, Ui_Dialog): @@ -69,3 +70,37 @@ class ProgressDialog(QDialog, Ui_Dialog): self._canceled() else: QDialog.keyPressEvent(self, ev) + +class BlockingBusy(QDialog): + + def __init__(self, msg, parent=None, window_title=_('Working')): + QDialog.__init__(self, parent) + + self._layout = QVBoxLayout() + self.setLayout(self._layout) + self.msg = QLabel(msg) + #self.msg.setWordWrap(True) + self.font = QFont() + self.font.setPointSize(self.font.pointSize() + 8) + self.msg.setFont(self.font) + self.pi = ProgressIndicator(self) + self.pi.setDisplaySize(100) + self._layout.addWidget(self.pi, 0, Qt.AlignHCenter) + self._layout.addSpacing(15) + self._layout.addWidget(self.msg, 0, Qt.AlignHCenter) + self.start() + self.setWindowTitle(window_title) + self.resize(self.sizeHint()) + + def start(self): + self.pi.startAnimation() + + def stop(self): + self.pi.stopAnimation() + + def accept(self): + self.stop() + return QDialog.accept(self) + + def reject(self): + pass # Cannot cancel this dialog diff --git a/src/calibre/gui2/progress_indicator/QProgressIndicator.cpp b/src/calibre/gui2/progress_indicator/QProgressIndicator.cpp index ce4e4364ad..095a4b9f68 100644 --- a/src/calibre/gui2/progress_indicator/QProgressIndicator.cpp +++ b/src/calibre/gui2/progress_indicator/QProgressIndicator.cpp @@ -1,124 +1,124 @@ -#include "QProgressIndicator.h" - -#include - -QProgressIndicator::QProgressIndicator(QWidget* parent, int size) - : QWidget(parent), - m_angle(0), - m_timerId(-1), - m_delay(80), - m_displayedWhenStopped(true), - m_displaySize(size), - m_color(Qt::black) -{ - setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - setFocusPolicy(Qt::NoFocus); -} - -bool QProgressIndicator::isAnimated () const -{ - return (m_timerId != -1); -} - -void QProgressIndicator::setDisplayedWhenStopped(bool state) -{ - m_displayedWhenStopped = state; - - update(); -} - -void QProgressIndicator::setDisplaySize(int size) -{ - m_displaySize = size; - update(); -} - - -bool QProgressIndicator::isDisplayedWhenStopped() const -{ - return m_displayedWhenStopped; -} - -void QProgressIndicator::startAnimation() -{ - m_angle = 0; - - if (m_timerId == -1) - m_timerId = startTimer(m_delay); -} - -void QProgressIndicator::stopAnimation() -{ - if (m_timerId != -1) - killTimer(m_timerId); - - m_timerId = -1; - - update(); -} - -void QProgressIndicator::setAnimationDelay(int delay) -{ - if (m_timerId != -1) - killTimer(m_timerId); - - m_delay = delay; - - if (m_timerId != -1) - m_timerId = startTimer(m_delay); -} - -void QProgressIndicator::setColor(const QColor & color) -{ - m_color = color; - - update(); -} - -QSize QProgressIndicator::sizeHint() const -{ - return QSize(m_displaySize, m_displaySize); -} - -int QProgressIndicator::heightForWidth(int w) const -{ - return w; -} - -void QProgressIndicator::timerEvent(QTimerEvent * /*event*/) -{ - m_angle = (m_angle+30)%360; - - update(); -} - -void QProgressIndicator::paintEvent(QPaintEvent * /*event*/) -{ - if (!m_displayedWhenStopped && !isAnimated()) - return; - - int width = qMin(this->width(), this->height()); - - QPainter p(this); - p.setRenderHint(QPainter::Antialiasing); - - int outerRadius = (width-1)*0.5; - int innerRadius = (width-1)*0.5*0.38; - - int capsuleHeight = outerRadius - innerRadius; - int capsuleWidth = (width > 32 ) ? capsuleHeight *.23 : capsuleHeight *.35; - int capsuleRadius = capsuleWidth/2; - - for (int i=0; i<12; i++) - { - QColor color = m_color; - color.setAlphaF(1.0f - (i/12.0f)); - p.setPen(Qt::NoPen); - p.setBrush(color); - p.save(); - p.translate(rect().center()); - p.rotate(m_angle - i*30.0f); - p.drawRoundedRect(-capsuleWidth*0.5, -(innerRadius+capsuleHeight), capsuleWidth, capsuleHeight, capsuleRadius, capsuleRadius); - p.restore(); - } -} +#include "QProgressIndicator.h" + +#include + +QProgressIndicator::QProgressIndicator(QWidget* parent, int size) + : QWidget(parent), + m_angle(0), + m_timerId(-1), + m_delay(80), + m_displayedWhenStopped(true), + m_displaySize(size), + m_color(Qt::black) +{ + setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + setFocusPolicy(Qt::NoFocus); +} + +bool QProgressIndicator::isAnimated () const +{ + return (m_timerId != -1); +} + +void QProgressIndicator::setDisplayedWhenStopped(bool state) +{ + m_displayedWhenStopped = state; + + update(); +} + +void QProgressIndicator::setDisplaySize(int size) +{ + m_displaySize = size; + update(); +} + + +bool QProgressIndicator::isDisplayedWhenStopped() const +{ + return m_displayedWhenStopped; +} + +void QProgressIndicator::startAnimation() +{ + m_angle = 0; + + if (m_timerId == -1) + m_timerId = startTimer(m_delay); +} + +void QProgressIndicator::stopAnimation() +{ + if (m_timerId != -1) + killTimer(m_timerId); + + m_timerId = -1; + + update(); +} + +void QProgressIndicator::setAnimationDelay(int delay) +{ + if (m_timerId != -1) + killTimer(m_timerId); + + m_delay = delay; + + if (m_timerId != -1) + m_timerId = startTimer(m_delay); +} + +void QProgressIndicator::setColor(const QColor & color) +{ + m_color = color; + + update(); +} + +QSize QProgressIndicator::sizeHint() const +{ + return QSize(m_displaySize, m_displaySize); +} + +int QProgressIndicator::heightForWidth(int w) const +{ + return w; +} + +void QProgressIndicator::timerEvent(QTimerEvent * /*event*/) +{ + m_angle = (m_angle+30)%360; + + update(); +} + +void QProgressIndicator::paintEvent(QPaintEvent * /*event*/) +{ + if (!m_displayedWhenStopped && !isAnimated()) + return; + + int width = qMin(this->width(), this->height()); + + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing); + + int outerRadius = (width-1)*0.5; + int innerRadius = (width-1)*0.5*0.38; + + int capsuleHeight = outerRadius - innerRadius; + int capsuleWidth = (width > 32 ) ? capsuleHeight *.23 : capsuleHeight *.35; + int capsuleRadius = capsuleWidth/2; + + for (int i=0; i<12; i++) + { + QColor color = m_color; + color.setAlphaF(1.0f - (i/12.0f)); + p.setPen(Qt::NoPen); + p.setBrush(color); + p.save(); + p.translate(rect().center()); + p.rotate(m_angle - i*30.0f); + p.drawRoundedRect(-capsuleWidth*0.5, -(innerRadius+capsuleHeight), capsuleWidth, capsuleHeight, capsuleRadius, capsuleRadius); + p.restore(); + } +} diff --git a/src/calibre/gui2/progress_indicator/QProgressIndicator.h b/src/calibre/gui2/progress_indicator/QProgressIndicator.h index e7e84cf95a..c2098ffe64 100644 --- a/src/calibre/gui2/progress_indicator/QProgressIndicator.h +++ b/src/calibre/gui2/progress_indicator/QProgressIndicator.h @@ -1,93 +1,93 @@ -#pragma once - -#include -#include - -/*! - \class QProgressIndicator - \brief The QProgressIndicator class lets an application display a progress indicator to show that a lengthy task is under way. - - Progress indicators are indeterminate and do nothing more than spin to show that the application is busy. - \sa QProgressBar -*/ -class QProgressIndicator : public QWidget -{ - Q_OBJECT - Q_PROPERTY(int delay READ animationDelay WRITE setAnimationDelay) - Q_PROPERTY(bool displayedWhenStopped READ isDisplayedWhenStopped WRITE setDisplayedWhenStopped) - Q_PROPERTY(QColor color READ color WRITE setColor) - Q_PROPERTY(int displaySize READ displaySize WRITE setDisplaySize) -public: - QProgressIndicator(QWidget* parent = 0, int size = 64); - - /*! Returns the delay between animation steps. - \return The number of milliseconds between animation steps. By default, the animation delay is set to 80 milliseconds. - \sa setAnimationDelay - */ - int animationDelay() const { return m_delay; } - - /*! Returns a Boolean value indicating whether the component is currently animated. - \return Animation state. - \sa startAnimation stopAnimation - */ - bool isAnimated () const; - - /*! Returns a Boolean value indicating whether the receiver shows itself even when it is not animating. - \return Return true if the progress indicator shows itself even when it is not animating. By default, it returns false. - \sa setDisplayedWhenStopped - */ - bool isDisplayedWhenStopped() const; - - /*! Returns the color of the component. - \sa setColor - */ - const QColor & color() const { return m_color; } - - virtual QSize sizeHint() const; - int heightForWidth(int w) const; - int displaySize() const { return m_displaySize; } -public slots: - /*! Starts the spin animation. - \sa stopAnimation isAnimated - */ - void startAnimation(); - - /*! Stops the spin animation. - \sa startAnimation isAnimated - */ - void stopAnimation(); - - /*! Sets the delay between animation steps. - Setting the \a delay to a value larger than 40 slows the animation, while setting the \a delay to a smaller value speeds it up. - \param delay The delay, in milliseconds. - \sa animationDelay - */ - void setAnimationDelay(int delay); - - /*! Sets whether the component hides itself when it is not animating. - \param state The animation state. Set false to hide the progress indicator when it is not animating; otherwise true. - \sa isDisplayedWhenStopped - */ - void setDisplayedWhenStopped(bool state); - - /*! Sets the color of the components to the given color. - \sa color - */ - void setColor(const QColor & color); - - /*! Set the size of this widget (used by sizeHint) - * \sa displaySize - */ - void setDisplaySize(int size); -protected: - virtual void timerEvent(QTimerEvent * event); - virtual void paintEvent(QPaintEvent * event); -private: - int m_angle; - int m_timerId; - int m_delay; - int m_displaySize; - bool m_displayedWhenStopped; - QColor m_color; -}; - +#pragma once + +#include +#include + +/*! + \class QProgressIndicator + \brief The QProgressIndicator class lets an application display a progress indicator to show that a lengthy task is under way. + + Progress indicators are indeterminate and do nothing more than spin to show that the application is busy. + \sa QProgressBar +*/ +class QProgressIndicator : public QWidget +{ + Q_OBJECT + Q_PROPERTY(int delay READ animationDelay WRITE setAnimationDelay) + Q_PROPERTY(bool displayedWhenStopped READ isDisplayedWhenStopped WRITE setDisplayedWhenStopped) + Q_PROPERTY(QColor color READ color WRITE setColor) + Q_PROPERTY(int displaySize READ displaySize WRITE setDisplaySize) +public: + QProgressIndicator(QWidget* parent = 0, int size = 64); + + /*! Returns the delay between animation steps. + \return The number of milliseconds between animation steps. By default, the animation delay is set to 80 milliseconds. + \sa setAnimationDelay + */ + int animationDelay() const { return m_delay; } + + /*! Returns a Boolean value indicating whether the component is currently animated. + \return Animation state. + \sa startAnimation stopAnimation + */ + bool isAnimated () const; + + /*! Returns a Boolean value indicating whether the receiver shows itself even when it is not animating. + \return Return true if the progress indicator shows itself even when it is not animating. By default, it returns false. + \sa setDisplayedWhenStopped + */ + bool isDisplayedWhenStopped() const; + + /*! Returns the color of the component. + \sa setColor + */ + const QColor & color() const { return m_color; } + + virtual QSize sizeHint() const; + int heightForWidth(int w) const; + int displaySize() const { return m_displaySize; } +public slots: + /*! Starts the spin animation. + \sa stopAnimation isAnimated + */ + void startAnimation(); + + /*! Stops the spin animation. + \sa startAnimation isAnimated + */ + void stopAnimation(); + + /*! Sets the delay between animation steps. + Setting the \a delay to a value larger than 40 slows the animation, while setting the \a delay to a smaller value speeds it up. + \param delay The delay, in milliseconds. + \sa animationDelay + */ + void setAnimationDelay(int delay); + + /*! Sets whether the component hides itself when it is not animating. + \param state The animation state. Set false to hide the progress indicator when it is not animating; otherwise true. + \sa isDisplayedWhenStopped + */ + void setDisplayedWhenStopped(bool state); + + /*! Sets the color of the components to the given color. + \sa color + */ + void setColor(const QColor & color); + + /*! Set the size of this widget (used by sizeHint) + * \sa displaySize + */ + void setDisplaySize(int size); +protected: + virtual void timerEvent(QTimerEvent * event); + virtual void paintEvent(QPaintEvent * event); +private: + int m_angle; + int m_timerId; + int m_delay; + int m_displaySize; + bool m_displayedWhenStopped; + QColor m_color; +}; + From 2464da207eadf16759352c256bdd988ab262d200 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 18 Aug 2010 06:58:08 +0100 Subject: [PATCH 48/73] Ensure restrictions with spaces in the name work in content server. --- src/calibre/library/server/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/library/server/base.py b/src/calibre/library/server/base.py index bdaee1c42d..ad1cd5b13a 100644 --- a/src/calibre/library/server/base.py +++ b/src/calibre/library/server/base.py @@ -104,7 +104,7 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache): def set_search_restriction(self, restriction): if restriction: - self.search_restriction = 'search:'+restriction + self.search_restriction = 'search:"%s"'%restriction else: self.search_restriction = '' From d3e2c90a231c342f522d9a99ce998a1b98a011fc Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 18 Aug 2010 08:08:28 +0100 Subject: [PATCH 49/73] Add bulk setting of the rest of the custom column types --- src/calibre/gui2/custom_column_widgets.py | 20 ++++++++-------- src/calibre/library/custom_columns.py | 29 +++++++++++++++++++---- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/src/calibre/gui2/custom_column_widgets.py b/src/calibre/gui2/custom_column_widgets.py index d624d5320d..8108454db3 100644 --- a/src/calibre/gui2/custom_column_widgets.py +++ b/src/calibre/gui2/custom_column_widgets.py @@ -403,9 +403,7 @@ class BulkBase(Base): val = self.getter() val = self.normalize_ui_val(val) if val != self.initial_val: - for book_id in book_ids: - QCoreApplication.processEvents() - self.db.set_custom(book_id, val, num=self.col_id, notify=notify) + self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify) class BulkBool(BulkBase, Bool): pass @@ -448,18 +446,21 @@ class BulkSeries(BulkBase): val = self.normalize_ui_val(val) update_indices = self.idx_widget.checkState() if val != '': + extras = [] + next_index = self.db.get_next_cc_series_num_for(val, num=self.col_id) for book_id in book_ids: QCoreApplication.processEvents() if update_indices: if tweaks['series_index_auto_increment'] == 'next': - s_index = self.db.get_next_cc_series_num_for\ - (val, num=self.col_id) + s_index = next_index + next_index += 1 else: s_index = 1.0 else: s_index = self.db.get_custom_extra(book_id, num=self.col_id, index_is_id=True) - self.db.set_custom(book_id, val, extra=s_index, + extras.append(s_index) + self.db.set_custom_bulk(book_ids, val, extras=extras, num=self.col_id, notify=notify) class RemoveTags(QWidget): @@ -540,14 +541,13 @@ class BulkText(BulkBase): add = set([v.strip() for v in txt.split(',')]) else: add = set() - self.db.set_custom_bulk(book_ids, add=add, remove=remove, num=self.col_id) + self.db.set_custom_bulk_multiple(book_ids, add=add, remove=remove, + num=self.col_id) else: val = self.getter() val = self.normalize_ui_val(val) if val != self.initial_val: - for book_id in book_ids: - QCoreApplication.processEvents() - self.db.set_custom(book_id, val, num=self.col_id, notify=notify) + self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify) def getter(self, original_value = None): if self.col_metadata['is_multiple']: diff --git a/src/calibre/library/custom_columns.py b/src/calibre/library/custom_columns.py index 7c613295b9..f02294a102 100644 --- a/src/calibre/library/custom_columns.py +++ b/src/calibre/library/custom_columns.py @@ -313,7 +313,7 @@ class CustomColumns(object): self.conn.commit() return changed - def set_custom_bulk(self, ids, add=[], remove=[], + def set_custom_bulk_multiple(self, ids, add=[], remove=[], label=None, num=None, notify=False): ''' Fast algorithm for updating custom column is_multiple datatypes. @@ -394,7 +394,30 @@ class CustomColumns(object): if notify: self.notify('metadata', ids) - def set_custom(self, id_, val, label=None, num=None, + def set_custom_bulk(self, ids, val, label=None, num=None, + append=False, notify=True, extras=None): + ''' + Change the value of a column for a set of books. The ids parameter is a + list of book ids to change. The extra field must be None or a list the + same length as ids. + ''' + if extras is not None and len(extras) != len(ids): + raise ValueError('Lentgh of ids and extras is not the same') + ev = None + for idx,id in enumerate(ids): + if extras is not None: + ev = extras[idx] + self._set_custom(id, val, label=label, num=num, append=append, + notify=notify, extra=ev) + self.conn.commit() + + def set_custom(self, id, val, label=None, num=None, + append=False, notify=True, extra=None): + self._set_custom(id, val, label=label, num=num, append=append, + notify=notify, extra=extra) + self.conn.commit() + + def _set_custom(self, id_, val, label=None, num=None, append=False, notify=True, extra=None): if label is not None: data = self.custom_column_label_map[label] @@ -450,7 +473,6 @@ class CustomColumns(object): self.conn.execute( '''INSERT INTO %s(book, value) VALUES (?,?)'''%lt, (id_, xid)) - self.conn.commit() nval = self.conn.get( 'SELECT custom_%s FROM meta2 WHERE id=?'%data['num'], (id_,), all=False) @@ -462,7 +484,6 @@ class CustomColumns(object): self.conn.execute( 'INSERT INTO %s(book,value) VALUES (?,?)'%table, (id_, val)) - self.conn.commit() nval = self.conn.get( 'SELECT custom_%s FROM meta2 WHERE id=?'%data['num'], (id_,), all=False) From fa17a92c2c97f264f2919c55aa271b780dc76da6 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 18 Aug 2010 10:11:11 +0100 Subject: [PATCH 50/73] 1) add restriction on startup to GUI 2) fix sorting problem in restriction combobx --- src/calibre/gui2/dialogs/config/__init__.py | 12 ++++- src/calibre/gui2/dialogs/config/config.ui | 50 ++++++++++++++------- src/calibre/gui2/search_box.py | 3 +- src/calibre/gui2/ui.py | 2 + 4 files changed, 48 insertions(+), 19 deletions(-) diff --git a/src/calibre/gui2/dialogs/config/__init__.py b/src/calibre/gui2/dialogs/config/__init__.py index c5b61f146d..2caaabc2cc 100644 --- a/src/calibre/gui2/dialogs/config/__init__.py +++ b/src/calibre/gui2/dialogs/config/__init__.py @@ -36,6 +36,7 @@ from calibre.gui2.convert.structure_detection import StructureDetectionWidget from calibre.ebooks.conversion.plumber import Plumber from calibre.utils.logging import Log from calibre.gui2.convert.toc import TOCWidget +from calibre.utils.search_query_parser import saved_searches class ConfigTabs(QTabWidget): @@ -447,7 +448,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog): self.password.setText(opts.password if opts.password else '') self.opt_max_opds_items.setValue(opts.max_opds_items) self.opt_max_opds_ungrouped_items.setValue(opts.max_opds_ungrouped_items) - self.opt_restriction.setText(self.db.prefs.get('cs_restriction', '')) + self.opt_cs_restriction.setText(self.db.prefs.get('cs_restriction', '')) self.auto_launch.setChecked(config['autolaunch_server']) self.systray_icon.setChecked(config['systray_icon']) self.sync_news.setChecked(config['upload_news_to_device']) @@ -494,6 +495,12 @@ class ConfigDialog(ResizableDialog, Ui_Dialog): if x == config['gui_layout']: li = i self.opt_gui_layout.setCurrentIndex(li) + restrictions = sorted(saved_searches().names(), + cmp=lambda x,y: cmp(x.lower(), y.lower())) + restrictions.insert(0, '') + self.opt_gui_restriction.addItems(restrictions) + idx = self.opt_gui_restriction.findText(self.db.prefs.get('gui_restriction', '')) + self.opt_gui_restriction.setCurrentIndex(0 if idx < 0 else idx) self.opt_disable_animations.setChecked(config['disable_animations']) self.opt_show_donate_button.setChecked(config['show_donate_button']) idx = 0 @@ -907,7 +914,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog): sc.set('max_opds_items', self.opt_max_opds_items.value()) sc.set('max_opds_ungrouped_items', self.opt_max_opds_ungrouped_items.value()) - self.db.prefs.set('cs_restriction', unicode(self.opt_restriction.text())) + self.db.prefs.set('cs_restriction', unicode(self.opt_cs_restriction.text())) config['delete_news_from_library_on_upload'] = self.delete_news.isChecked() config['upload_news_to_device'] = self.sync_news.isChecked() config['search_as_you_type'] = self.search_as_you_type.isChecked() @@ -929,6 +936,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog): config['internally_viewed_formats'] = fmts val = self.opt_gui_layout.itemData(self.opt_gui_layout.currentIndex()).toString() config['gui_layout'] = unicode(val) + self.db.prefs.set('gui_restriction', unicode(self.opt_gui_restriction.currentText())) if must_restart: warning_dialog(self, _('Must restart'), diff --git a/src/calibre/gui2/dialogs/config/config.ui b/src/calibre/gui2/dialogs/config/config.ui index 62eb7bb620..3c5c109e04 100644 --- a/src/calibre/gui2/dialogs/config/config.ui +++ b/src/calibre/gui2/dialogs/config/config.ui @@ -295,7 +295,7 @@ - + Use &Roman numerals for series number @@ -305,35 +305,35 @@ - + Enable system &tray icon (needs restart) - + Show &notifications in system tray - + Show &splash screen at startup - + Show cover &browser in a separate window (needs restart) - + Show &average ratings in the tags browser @@ -343,7 +343,7 @@ - + Search as you type @@ -353,21 +353,21 @@ - + Automatically send downloaded &news to ebook reader - + &Delete news from library when it is automatically sent to reader - + @@ -384,7 +384,7 @@ - + @@ -570,7 +570,27 @@ + + + + Restriction to apply when the current library is opened (startup or change library): + + + opt_gui_restriction + + + + + + + 250 + 16777215 + + + + + Disable all animations. Useful if you have a slow/old computer. @@ -580,14 +600,14 @@ - + Show &donate button (restart) - + &Toolbar @@ -1041,7 +1061,7 @@ - + Provides a restriction to be used by the content server @@ -1056,7 +1076,7 @@ Restriction (saved search) to apply: - opt_restriction + opt_cs_restriction diff --git a/src/calibre/gui2/search_box.py b/src/calibre/gui2/search_box.py index 95667379a1..7169eb5fd3 100644 --- a/src/calibre/gui2/search_box.py +++ b/src/calibre/gui2/search_box.py @@ -402,8 +402,7 @@ class SavedSearchBoxMixin(object): b.setStatusTip(b.toolTip()) def saved_searches_changed(self): - p = saved_searches().names() - p.sort() + p = sorted(saved_searches().names(), cmp=lambda x,y: cmp(x.lower(), y.lower())) t = unicode(self.search_restriction.currentText()) self.search_restriction.clear() # rebuild the restrictions combobox using current saved searches self.search_restriction.addItem('') diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index f521bceaf9..41b166b13f 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -230,6 +230,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{ ######################### Search Restriction ########################## SearchRestrictionMixin.__init__(self) + self.apply_named_search_restriction(db.prefs.get('gui_restriction', '')) ########################### Cover Flow ################################ @@ -373,6 +374,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{ self.set_window_title() self.apply_named_search_restriction('') # reset restriction to null self.saved_searches_changed() # reload the search restrictions combo box + self.apply_named_search_restriction(db.prefs.get('gui_restriction', '')) def set_window_title(self): self.setWindowTitle(__appname__ + u' - ||%s||'%self.iactions['Choose Library'].library_name()) From 8521a4b6989d1c06a35c73617753d2a39dee76c8 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 18 Aug 2010 10:56:40 +0100 Subject: [PATCH 51/73] Fix #6537 - series order on sony being lost --- src/calibre/devices/usbms/books.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/devices/usbms/books.py b/src/calibre/devices/usbms/books.py index cdba980642..2b304da27f 100644 --- a/src/calibre/devices/usbms/books.py +++ b/src/calibre/devices/usbms/books.py @@ -181,7 +181,7 @@ class CollectionsBookList(BookList): if lpath not in collections_lpaths[category]: collections_lpaths[category].add(lpath) collections[category].append(book) - if attr == 'series': + if attr == 'series' or getattr(book, 'series', None) == category: series_categories.add(category) # Sort collections for category, books in collections.items(): From e2b093f6ab039027515ce1bfbb2180c58818394c Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 18 Aug 2010 11:23:19 +0100 Subject: [PATCH 52/73] Improve the fix for #6537 - series order on sony being lost --- src/calibre/devices/usbms/books.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/calibre/devices/usbms/books.py b/src/calibre/devices/usbms/books.py index 2b304da27f..959f26199c 100644 --- a/src/calibre/devices/usbms/books.py +++ b/src/calibre/devices/usbms/books.py @@ -181,7 +181,9 @@ class CollectionsBookList(BookList): if lpath not in collections_lpaths[category]: collections_lpaths[category].add(lpath) collections[category].append(book) - if attr == 'series' or getattr(book, 'series', None) == category: + if attr == 'series' or \ + ('series' in collection_attributes and + getattr(book, 'series', None) == category): series_categories.add(category) # Sort collections for category, books in collections.items(): From 0fcf26a113c416e0af95b316b99f6634be72d312 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 18 Aug 2010 13:09:59 +0100 Subject: [PATCH 53/73] Enhancement #6237 - automatically connect to a folder on startup --- resources/default_tweaks.py | 8 ++++++++ src/calibre/gui2/device.py | 9 ++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/resources/default_tweaks.py b/resources/default_tweaks.py index d6f134f724..07aee5c6fa 100644 --- a/resources/default_tweaks.py +++ b/resources/default_tweaks.py @@ -81,3 +81,11 @@ title_series_sorting = 'library_order' # strictly_alphabetic, it would remain "The Client". save_template_title_series_sorting = 'library_order' +# Specify a folder that calibre should connect to at startup using +# connect_to_folder. This must be a full path to the folder. If the folder does +# not exist when calibre starts, it is ignored. If there are '\' characters in +# the path (such as in Windows paths), you must double them. +# Examples: +# auto_connect_to_folder = 'C:\\Users\\someone\\Desktop\\testlib' +# auto_connect_to_folder = '/home/dropbox/My Dropbox/someone/library' +auto_connect_to_folder = '' \ No newline at end of file diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index a9beb317a2..6791c4b015 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -33,7 +33,7 @@ from calibre.devices.apple.driver import ITUNES_ASYNC from calibre.devices.folder_device.driver import FOLDER_DEVICE from calibre.ebooks.metadata.meta import set_metadata from calibre.constants import DEBUG -from calibre.utils.config import prefs +from calibre.utils.config import prefs, tweaks # }}} @@ -613,6 +613,8 @@ class DeviceMixin(object): # {{{ self.device_manager = DeviceManager(Dispatcher(self.device_detected), self.job_manager, Dispatcher(self.status_bar.show_message)) self.device_manager.start() + if tweaks['auto_connect_to_folder']: + self.connect_to_folder_named(tweaks['auto_connect_to_folder']) def set_default_thumbnail(self, height): r = QSvgRenderer(I('book.svg')) @@ -624,6 +626,11 @@ class DeviceMixin(object): # {{{ self.default_thumbnail = (pixmap.width(), pixmap.height(), pixmap_to_data(pixmap)) + def connect_to_folder_named(self, dir): + if os.path.isdir(dir): + kls = FOLDER_DEVICE + self.device_manager.mount_device(kls=kls, kind='folder', path=dir) + def connect_to_folder(self): dir = choose_dir(self, 'Select Device Folder', _('Select folder to open as device')) From c02df37c780d5a4bb47e4951427b3fc6e85ae612 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 18 Aug 2010 09:50:58 -0600 Subject: [PATCH 54/73] Run bulk metadata updates in a separate thread --- src/calibre/gui2/custom_column_widgets.py | 62 +++++++++------ src/calibre/gui2/dialogs/metadata_bulk.py | 94 +++++++++++++---------- 2 files changed, 90 insertions(+), 66 deletions(-) diff --git a/src/calibre/gui2/custom_column_widgets.py b/src/calibre/gui2/custom_column_widgets.py index d624d5320d..6bd2f3c77a 100644 --- a/src/calibre/gui2/custom_column_widgets.py +++ b/src/calibre/gui2/custom_column_widgets.py @@ -11,7 +11,7 @@ from functools import partial from PyQt4.Qt import QComboBox, QLabel, QSpinBox, QDoubleSpinBox, QDateEdit, \ QDate, QGroupBox, QVBoxLayout, QPlainTextEdit, QSizePolicy, \ QSpacerItem, QIcon, QCheckBox, QWidget, QHBoxLayout, SIGNAL, \ - QPushButton, QCoreApplication + QPushButton from calibre.utils.date import qt_to_dt, now from calibre.gui2.widgets import TagsLineEdit, EnComboBox @@ -32,8 +32,13 @@ class Base(object): val = self.normalize_db_val(val) self.setter(val) + @property + def gui_val(self): + return self.getter() + + def commit(self, book_id, notify=False): - val = self.getter() + val = self.gui_val val = self.normalize_ui_val(val) if val != self.initial_val: self.db.set_custom(book_id, val, num=self.col_id, notify=notify) @@ -284,10 +289,14 @@ class Series(Base): if idx is not None: self.widgets[1].setCurrentIndex(idx) + def getter(self): + n = unicode(self.name_widget.currentText()).strip() + i = self.idx_widget.value() + return n, i + def commit(self, book_id, notify=False): - val = unicode(self.name_widget.currentText()).strip() + val, s_index = self.gui_val val = self.normalize_ui_val(val) - s_index = self.idx_widget.value() if val != self.initial_val or s_index != self.initial_index: if s_index == 0.0: if tweaks['series_index_auto_increment'] == 'next': @@ -378,6 +387,13 @@ def populate_metadata_page(layout, db, book_id, bulk=False, two_column=False, pa class BulkBase(Base): + @property + def gui_val(self): + if not hasattr(self, '_cached_gui_val_'): + self._cached_gui_val_ = self.getter() + return self._cached_gui_val_ + + def get_initial_value(self, book_ids): values = set([]) for book_id in book_ids: @@ -400,11 +416,10 @@ class BulkBase(Base): self.setter(val) def commit(self, book_ids, notify=False): - val = self.getter() + val = self.gui_val val = self.normalize_ui_val(val) if val != self.initial_val: for book_id in book_ids: - QCoreApplication.processEvents() self.db.set_custom(book_id, val, num=self.col_id, notify=notify) class BulkBool(BulkBase, Bool): @@ -443,13 +458,16 @@ class BulkSeries(BulkBase): self.name_widget.addItem(c) self.name_widget.setEditText('') + def getter(self): + n = unicode(self.name_widget.currentText()).strip() + i = self.idx_widget.checkState() + return n, i + def commit(self, book_ids, notify=False): - val = unicode(self.name_widget.currentText()).strip() + val, update_indices = self.gui_val val = self.normalize_ui_val(val) - update_indices = self.idx_widget.checkState() if val != '': for book_id in book_ids: - QCoreApplication.processEvents() if update_indices: if tweaks['series_index_auto_increment'] == 'next': s_index = self.db.get_next_cc_series_num_for\ @@ -526,41 +544,35 @@ class BulkText(BulkBase): def commit(self, book_ids, notify=False): if self.col_metadata['is_multiple']: + remove_all, adding, rtext = self.gui_val remove = set() - if self.removing_widget.checkbox.isChecked(): + if remove_all: for book_id in book_ids: remove |= set(self.db.get_custom(book_id, num=self.col_id, index_is_id=True)) else: - txt = unicode(self.removing_widget.tags_box.text()) + txt = rtext if txt: remove = set([v.strip() for v in txt.split(',')]) - txt = unicode(self.adding_widget.text()) + txt = adding if txt: add = set([v.strip() for v in txt.split(',')]) else: add = set() self.db.set_custom_bulk(book_ids, add=add, remove=remove, num=self.col_id) else: - val = self.getter() + val = self.gui_val val = self.normalize_ui_val(val) if val != self.initial_val: for book_id in book_ids: - QCoreApplication.processEvents() self.db.set_custom(book_id, val, num=self.col_id, notify=notify) - def getter(self, original_value = None): + def getter(self): if self.col_metadata['is_multiple']: - if self.removing_widget.checkbox.isChecked(): - ans = set() - else: - ans = set(original_value) - ans -= set([v.strip() for v in - unicode(self.removing_widget.tags_box.text()).split(',')]) - txt = unicode(self.adding_widget.text()) - if txt: - ans |= set([v.strip() for v in txt.split(',')]) - return ans # returning a set instead of a list works, for now at least. + return self.removing_widget.checkbox.isChecked(), \ + unicode(self.adding_widget.text()), \ + unicode(self.removing_widget.tags_box.text()) + val = unicode(self.widgets[1].currentText()).strip() if not val: val = None diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index 0139d0aee2..c216463941 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -4,14 +4,33 @@ __copyright__ = '2008, Kovid Goyal ' '''Dialog to edit metadata in bulk''' from PyQt4.Qt import SIGNAL, QObject, QDialog, QGridLayout, \ - QCoreApplication + QThread, Qt from calibre.gui2.dialogs.metadata_bulk_ui import Ui_MetadataBulkDialog from calibre.gui2.dialogs.tag_editor import TagEditor from calibre.ebooks.metadata import string_to_authors, \ authors_to_string from calibre.gui2.custom_column_widgets import populate_metadata_page -from calibre.gui2.dialogs.progress import ProgressDialog +from calibre.gui2.dialogs.progress import BlockingBusy +from calibre.gui2 import error_dialog + +class Worker(QThread): + + def __init__(self, func, parent=None): + QThread.__init__(self, parent) + self.func = func + self.error = None + + def run(self): + try: + self.func() + except Exception, err: + import traceback + try: + err = unicode(err) + except: + err = repr(err) + self.error = (err, traceback.format_exc()) class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): @@ -107,35 +126,29 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): if len(self.ids) < 1: return QDialog.accept(self) - pd = ProgressDialog(_('Working'), - _('Applying changes to %d books. This may take a while.')%len(self.ids), - 0, 0, self, cancelable=False) - pd.setModal(True) - pd.show() - def upd(): - QCoreApplication.processEvents() + remove = unicode(self.remove_tags.text()).strip().split(',') + add = unicode(self.tags.text()).strip().split(',') + au = unicode(self.authors.text()) + aus = unicode(self.author_sort.text()) + do_aus = self.author_sort.isEnabled() + rating = self.rating.value() + pub = unicode(self.publisher.text()) + do_series = self.write_series + series = unicode(self.series.currentText()).strip() + do_autonumber = self.autonumber_series.isChecked() + do_remove_format = self.remove_format.currentIndex() > -1 + remove_format = unicode(self.remove_format.currentText()) + do_swap_ta = self.swap_title_and_author.isChecked() + do_remove_conv = self.remove_conversion_settings.isChecked() + do_auto_author = self.auto_author_sort.isChecked() + self.changed = bool(self.ids) + # Cache values from GUI so that Qt widgets are not used in + # non GUI thread + for w in getattr(self, 'custom_column_widgets', []): + w.gui_val - try: - remove = unicode(self.remove_tags.text()).strip().split(',') - add = unicode(self.tags.text()).strip().split(',') - au = unicode(self.authors.text()) - aus = unicode(self.author_sort.text()) - do_aus = self.author_sort.isEnabled() - rating = self.rating.value() - pub = unicode(self.publisher.text()) - do_series = self.write_series - series = unicode(self.series.currentText()).strip() - do_autonumber = self.autonumber_series.isChecked() - do_remove_format = self.remove_format.currentIndex() > -1 - remove_format = unicode(self.remove_format.currentText()) - do_swap_ta = self.swap_title_and_author.isChecked() - do_remove_conv = self.remove_conversion_settings.isChecked() - do_auto_author = self.auto_author_sort.isChecked() - - upd() - self.changed = bool(self.ids) + def doit(): for id in self.ids: - upd() if do_swap_ta: title = self.db.title(id, index_is_id=True) aum = self.db.authors(id, index_is_id=True) @@ -146,55 +159,54 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): if title: new_authors = string_to_authors(title) self.db.set_authors(id, new_authors, notify=False) - upd() if au: self.db.set_authors(id, string_to_authors(au), notify=False) - upd() if do_auto_author: x = self.db.author_sort_from_book(id, index_is_id=True) if x: self.db.set_author_sort(id, x, notify=False) - upd() if aus and do_aus: self.db.set_author_sort(id, aus, notify=False) - upd() if rating != -1: self.db.set_rating(id, 2*rating, notify=False) if pub: self.db.set_publisher(id, pub, notify=False) - upd() if do_series: next = self.db.get_next_series_num_for(series) self.db.set_series(id, series, notify=False) num = next if do_autonumber and series else 1.0 self.db.set_series_index(id, num, notify=False) - upd() if do_remove_format: self.db.remove_format(id, remove_format, index_is_id=True, notify=False) - upd() if do_remove_conv: self.db.delete_conversion_options(id, 'PIPE') - upd() for w in getattr(self, 'custom_column_widgets', []): w.commit(self.ids) self.db.bulk_modify_tags(self.ids, add=add, remove=remove, notify=False) - upd() + self.db.clean() + self.worker = Worker(doit, self) + self.worker.start() - finally: - pd.hide() + bb = BlockingBusy(_('Applying changes to %d books. This may take a while.') + %len(self.ids), parent=self) + self.worker.finished.connect(bb.accept, type=Qt.QueuedConnection) + bb.exec_() - self.db.clean() + if self.worker.error is not None: + return error_dialog(self, _('Failed'), + self.worker.error[0], det_msg=self.worker.error[1], + show=True) return QDialog.accept(self) From 9c64ce80683c3622bde8ab215e732e9b3e9a5c9c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 18 Aug 2010 09:57:25 -0600 Subject: [PATCH 55/73] Content Server: Do not add empty categories to the top level OPDS feed --- src/calibre/library/server/opds.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/calibre/library/server/opds.py b/src/calibre/library/server/opds.py index f32e5ad47a..e1cbb79599 100644 --- a/src/calibre/library/server/opds.py +++ b/src/calibre/library/server/opds.py @@ -550,6 +550,8 @@ class OPDSServer(object): (_('Title'), _('Title'), 'Otitle'), ] for category in categories: + if len(categories[category]) == 0: + continue if category == 'formats': continue meta = category_meta.get(category, None) From 39c820043db89dc5fc688a83b038837cd5df1b53 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 18 Aug 2010 10:06:00 -0600 Subject: [PATCH 56/73] Speed up bulk editing on non is_multiple custom columns as well --- src/calibre/gui2/custom_column_widgets.py | 18 +++++++------- src/calibre/library/custom_columns.py | 29 +++++++++++++++++++---- 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/src/calibre/gui2/custom_column_widgets.py b/src/calibre/gui2/custom_column_widgets.py index 6bd2f3c77a..4a1a85f159 100644 --- a/src/calibre/gui2/custom_column_widgets.py +++ b/src/calibre/gui2/custom_column_widgets.py @@ -419,8 +419,7 @@ class BulkBase(Base): val = self.gui_val val = self.normalize_ui_val(val) if val != self.initial_val: - for book_id in book_ids: - self.db.set_custom(book_id, val, num=self.col_id, notify=notify) + self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify) class BulkBool(BulkBase, Bool): pass @@ -467,17 +466,20 @@ class BulkSeries(BulkBase): val, update_indices = self.gui_val val = self.normalize_ui_val(val) if val != '': + extras = [] + next_index = self.db.get_next_cc_series_num_for(val, num=self.col_id) for book_id in book_ids: if update_indices: if tweaks['series_index_auto_increment'] == 'next': - s_index = self.db.get_next_cc_series_num_for\ - (val, num=self.col_id) + s_index = next_index + next_index += 1 else: s_index = 1.0 else: s_index = self.db.get_custom_extra(book_id, num=self.col_id, index_is_id=True) - self.db.set_custom(book_id, val, extra=s_index, + extras.append(s_index) + self.db.set_custom_bulk(book_ids, val, extras=extras, num=self.col_id, notify=notify) class RemoveTags(QWidget): @@ -559,13 +561,13 @@ class BulkText(BulkBase): add = set([v.strip() for v in txt.split(',')]) else: add = set() - self.db.set_custom_bulk(book_ids, add=add, remove=remove, num=self.col_id) + self.db.set_custom_bulk_multiple(book_ids, add=add, remove=remove, + num=self.col_id) else: val = self.gui_val val = self.normalize_ui_val(val) if val != self.initial_val: - for book_id in book_ids: - self.db.set_custom(book_id, val, num=self.col_id, notify=notify) + self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify) def getter(self): if self.col_metadata['is_multiple']: diff --git a/src/calibre/library/custom_columns.py b/src/calibre/library/custom_columns.py index 7c613295b9..f02294a102 100644 --- a/src/calibre/library/custom_columns.py +++ b/src/calibre/library/custom_columns.py @@ -313,7 +313,7 @@ class CustomColumns(object): self.conn.commit() return changed - def set_custom_bulk(self, ids, add=[], remove=[], + def set_custom_bulk_multiple(self, ids, add=[], remove=[], label=None, num=None, notify=False): ''' Fast algorithm for updating custom column is_multiple datatypes. @@ -394,7 +394,30 @@ class CustomColumns(object): if notify: self.notify('metadata', ids) - def set_custom(self, id_, val, label=None, num=None, + def set_custom_bulk(self, ids, val, label=None, num=None, + append=False, notify=True, extras=None): + ''' + Change the value of a column for a set of books. The ids parameter is a + list of book ids to change. The extra field must be None or a list the + same length as ids. + ''' + if extras is not None and len(extras) != len(ids): + raise ValueError('Lentgh of ids and extras is not the same') + ev = None + for idx,id in enumerate(ids): + if extras is not None: + ev = extras[idx] + self._set_custom(id, val, label=label, num=num, append=append, + notify=notify, extra=ev) + self.conn.commit() + + def set_custom(self, id, val, label=None, num=None, + append=False, notify=True, extra=None): + self._set_custom(id, val, label=label, num=num, append=append, + notify=notify, extra=extra) + self.conn.commit() + + def _set_custom(self, id_, val, label=None, num=None, append=False, notify=True, extra=None): if label is not None: data = self.custom_column_label_map[label] @@ -450,7 +473,6 @@ class CustomColumns(object): self.conn.execute( '''INSERT INTO %s(book, value) VALUES (?,?)'''%lt, (id_, xid)) - self.conn.commit() nval = self.conn.get( 'SELECT custom_%s FROM meta2 WHERE id=?'%data['num'], (id_,), all=False) @@ -462,7 +484,6 @@ class CustomColumns(object): self.conn.execute( 'INSERT INTO %s(book,value) VALUES (?,?)'%table, (id_, val)) - self.conn.commit() nval = self.conn.get( 'SELECT custom_%s FROM meta2 WHERE id=?'%data['num'], (id_,), all=False) From 1c8d6e7c73bfdb41d06a63de870e0d5f5de00a16 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 18 Aug 2010 10:11:52 -0600 Subject: [PATCH 57/73] SONY driver: Fix series order being lost when metadata management is set to manual --- src/calibre/devices/usbms/books.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/calibre/devices/usbms/books.py b/src/calibre/devices/usbms/books.py index cdba980642..959f26199c 100644 --- a/src/calibre/devices/usbms/books.py +++ b/src/calibre/devices/usbms/books.py @@ -181,7 +181,9 @@ class CollectionsBookList(BookList): if lpath not in collections_lpaths[category]: collections_lpaths[category].add(lpath) collections[category].append(book) - if attr == 'series': + if attr == 'series' or \ + ('series' in collection_attributes and + getattr(book, 'series', None) == category): series_categories.add(category) # Sort collections for category, books in collections.items(): From 56e5ccbd02f5c1909b0274c24f1aeeb70ec4b197 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 18 Aug 2010 18:58:01 +0100 Subject: [PATCH 58/73] First iteration of bulk metadata edit w/o transactions --- src/calibre/gui2/dialogs/metadata_bulk.py | 22 +++++--- src/calibre/gui2/dialogs/progress.py | 12 +++-- src/calibre/library/database2.py | 63 +++++++++++++---------- 3 files changed, 58 insertions(+), 39 deletions(-) diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index c216463941..05fb5ec6e4 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -148,6 +148,9 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): w.gui_val def doit(): + # author and title changes cause file system changes as well as + # database changes. Do these as individual transactions, so that + # exceptions do the smallest amount of damage for id in self.ids: if do_swap_ta: title = self.db.title(id, index_is_id=True) @@ -159,36 +162,39 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): if title: new_authors = string_to_authors(title) self.db.set_authors(id, new_authors, notify=False) - if au: self.db.set_authors(id, string_to_authors(au), notify=False) + # all of the following are database-only operations, so do them + # as a single transaction + for id in self.ids: if do_auto_author: x = self.db.author_sort_from_book(id, index_is_id=True) if x: - self.db.set_author_sort(id, x, notify=False) + self.db.set_author_sort(id, x, notify=False, commit=False) if aus and do_aus: - self.db.set_author_sort(id, aus, notify=False) + self.db.set_author_sort(id, aus, notify=False, commit=False) if rating != -1: - self.db.set_rating(id, 2*rating, notify=False) + self.db.set_rating(id, 2*rating, notify=False, commit=False) if pub: - self.db.set_publisher(id, pub, notify=False) + self.db.set_publisher(id, pub, notify=False, commit=False) if do_series: next = self.db.get_next_series_num_for(series) - self.db.set_series(id, series, notify=False) + self.db.set_series(id, series, notify=False, commit=False) num = next if do_autonumber and series else 1.0 - self.db.set_series_index(id, num, notify=False) + self.db.set_series_index(id, num, notify=False, commit=False) if do_remove_format: - self.db.remove_format(id, remove_format, index_is_id=True, notify=False) + self.db.remove_format(id, remove_format, index_is_id=True, notify=False, commit=False) if do_remove_conv: self.db.delete_conversion_options(id, 'PIPE') + self.db.conn.commit() for w in getattr(self, 'custom_column_widgets', []): w.commit(self.ids) self.db.bulk_modify_tags(self.ids, add=add, remove=remove, diff --git a/src/calibre/gui2/dialogs/progress.py b/src/calibre/gui2/dialogs/progress.py index 553ee4b03b..79b4af5ea4 100644 --- a/src/calibre/gui2/dialogs/progress.py +++ b/src/calibre/gui2/dialogs/progress.py @@ -83,9 +83,9 @@ class BlockingBusy(QDialog): self.font = QFont() self.font.setPointSize(self.font.pointSize() + 8) self.msg.setFont(self.font) - self.pi = ProgressIndicator(self) - self.pi.setDisplaySize(100) - self._layout.addWidget(self.pi, 0, Qt.AlignHCenter) +# self.pi = ProgressIndicator(self) +# self.pi.setDisplaySize(100) +# self._layout.addWidget(self.pi, 0, Qt.AlignHCenter) self._layout.addSpacing(15) self._layout.addWidget(self.msg, 0, Qt.AlignHCenter) self.start() @@ -93,10 +93,12 @@ class BlockingBusy(QDialog): self.resize(self.sizeHint()) def start(self): - self.pi.startAnimation() +# self.pi.startAnimation() + pass def stop(self): - self.pi.stopAnimation() +# self.pi.stopAnimation() + pass def accept(self): self.stop() diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 6728735eb3..cea9c8efa4 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -389,7 +389,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): path = path.lower() return path - def set_path(self, index, index_is_id=False): + def set_path(self, index, index_is_id=False, commit=True): ''' Set the path to the directory containing this books files based on its current title and author. If there was a previous directory, its contents @@ -428,7 +428,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): stream = cStringIO.StringIO(f) self.add_format(id, format, stream, index_is_id=True, path=tpath) self.conn.execute('UPDATE books SET path=? WHERE id=?', (path, id)) - self.conn.commit() + if commit: + self.conn.commit() self.data.set(id, self.FIELD_MAP['path'], path, row_is_id=True) # Delete not needed directories if current_path and os.path.exists(spath): @@ -729,7 +730,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): if notify: self.notify('delete', [id]) - def remove_format(self, index, format, index_is_id=False, notify=True): + def remove_format(self, index, format, index_is_id=False, notify=True, commit=True): id = index if index_is_id else self.id(index) name = self.conn.get('SELECT name FROM data WHERE book=? AND format=?', (id, format), all=False) if name: @@ -739,7 +740,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): except: traceback.print_exc() self.conn.execute('DELETE FROM data WHERE book=? AND format=?', (id, format.upper())) - self.conn.commit() + if commit: + self.conn.commit() self.refresh_ids([id]) if notify: self.notify('metadata', [id]) @@ -1016,7 +1018,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): if mi.series: doit(self.set_series, id, mi.series, notify=False) if mi.cover_data[1] is not None: - doit(self.set_cover, id, mi.cover_data[1]) + doit(self.set_cover, id, mi.cover_data[1]) # doesn't use commit elif mi.cover is not None and os.access(mi.cover, os.R_OK): doit(self.set_cover, id, open(mi.cover, 'rb')) if mi.tags: @@ -1092,7 +1094,6 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): (id, aid)) except IntegrityError: # Sometimes books specify the same author twice in their metadata pass - self.conn.commit() ss = self.author_sort_from_book(id, index_is_id=True) self.conn.execute('UPDATE books SET author_sort=? WHERE id=?', (ss, id)) @@ -1121,24 +1122,26 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): if notify: self.notify('metadata', [id]) - def set_timestamp(self, id, dt, notify=True): + def set_timestamp(self, id, dt, notify=True, commit=True): if dt: self.conn.execute('UPDATE books SET timestamp=? WHERE id=?', (dt, id)) self.data.set(id, self.FIELD_MAP['timestamp'], dt, row_is_id=True) - self.conn.commit() + if commit: + self.conn.commit() if notify: self.notify('metadata', [id]) - def set_pubdate(self, id, dt, notify=True): + def set_pubdate(self, id, dt, notify=True, commit=True): if dt: self.conn.execute('UPDATE books SET pubdate=? WHERE id=?', (dt, id)) self.data.set(id, self.FIELD_MAP['pubdate'], dt, row_is_id=True) - self.conn.commit() + if commit: + self.conn.commit() if notify: self.notify('metadata', [id]) - def set_publisher(self, id, publisher, notify=True): + def set_publisher(self, id, publisher, notify=True, commit=True): self.conn.execute('DELETE FROM books_publishers_link WHERE book=?',(id,)) self.conn.execute('DELETE FROM publishers WHERE (SELECT COUNT(id) FROM books_publishers_link WHERE publisher=publishers.id) < 1') if publisher: @@ -1150,7 +1153,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): else: aid = self.conn.execute('INSERT INTO publishers(name) VALUES (?)', (publisher,)).lastrowid self.conn.execute('INSERT INTO books_publishers_link(book, publisher) VALUES (?,?)', (id, aid)) - self.conn.commit() + if commit: + self.conn.commit() self.data.set(id, self.FIELD_MAP['publisher'], publisher, row_is_id=True) if notify: self.notify('metadata', [id]) @@ -1447,7 +1451,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): if notify: self.notify('metadata', ids) - def set_tags(self, id, tags, append=False, notify=True): + def set_tags(self, id, tags, append=False, notify=True, commit=True): ''' @param tags: list of strings @param append: If True existing tags are not removed @@ -1481,7 +1485,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): (id, tid), all=False): self.conn.execute('INSERT INTO books_tags_link(book, tag) VALUES (?,?)', (id, tid)) - self.conn.commit() + if commit: + self.conn.commit() tags = u','.join(self.get_tags(id)) self.data.set(id, self.FIELD_MAP['tags'], tags, row_is_id=True) if notify: @@ -1520,7 +1525,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.conn.execute('DELETE FROM tags WHERE id=?', (id,)) self.conn.commit() - def set_series(self, id, series, notify=True): + def set_series(self, id, series, notify=True, commit=True): self.conn.execute('DELETE FROM books_series_link WHERE book=?',(id,)) self.conn.execute('DELETE FROM series WHERE (SELECT COUNT(id) FROM books_series_link WHERE series=series.id) < 1') if series: @@ -1534,12 +1539,13 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): else: aid = self.conn.execute('INSERT INTO series(name) VALUES (?)', (series,)).lastrowid self.conn.execute('INSERT INTO books_series_link(book, series) VALUES (?,?)', (id, aid)) - self.conn.commit() + if commit: + self.conn.commit() self.data.set(id, self.FIELD_MAP['series'], series, row_is_id=True) if notify: self.notify('metadata', [id]) - def set_series_index(self, id, idx, notify=True): + def set_series_index(self, id, idx, notify=True, commit=True): if idx is None: idx = 1.0 try: @@ -1547,40 +1553,45 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): except: idx = 1.0 self.conn.execute('UPDATE books SET series_index=? WHERE id=?', (idx, id)) - self.conn.commit() + if commit: + self.conn.commit() self.data.set(id, self.FIELD_MAP['series_index'], idx, row_is_id=True) if notify: self.notify('metadata', [id]) - def set_rating(self, id, rating, notify=True): + def set_rating(self, id, rating, notify=True, commit=True): rating = int(rating) self.conn.execute('DELETE FROM books_ratings_link WHERE book=?',(id,)) rat = self.conn.get('SELECT id FROM ratings WHERE rating=?', (rating,), all=False) rat = rat if rat else self.conn.execute('INSERT INTO ratings(rating) VALUES (?)', (rating,)).lastrowid self.conn.execute('INSERT INTO books_ratings_link(book, rating) VALUES (?,?)', (id, rat)) - self.conn.commit() + if commit: + self.conn.commit() self.data.set(id, self.FIELD_MAP['rating'], rating, row_is_id=True) if notify: self.notify('metadata', [id]) - def set_comment(self, id, text, notify=True): + def set_comment(self, id, text, notify=True, commit=True): self.conn.execute('DELETE FROM comments WHERE book=?', (id,)) self.conn.execute('INSERT INTO comments(book,text) VALUES (?,?)', (id, text)) - self.conn.commit() + if commit: + self.conn.commit() self.data.set(id, self.FIELD_MAP['comments'], text, row_is_id=True) if notify: self.notify('metadata', [id]) - def set_author_sort(self, id, sort, notify=True): + def set_author_sort(self, id, sort, notify=True, commit=True): self.conn.execute('UPDATE books SET author_sort=? WHERE id=?', (sort, id)) - self.conn.commit() + if commit: + self.conn.commit() self.data.set(id, self.FIELD_MAP['author_sort'], sort, row_is_id=True) if notify: self.notify('metadata', [id]) - def set_isbn(self, id, isbn, notify=True): + def set_isbn(self, id, isbn, notify=True, commit=True): self.conn.execute('UPDATE books SET isbn=? WHERE id=?', (isbn, id)) - self.conn.commit() + if commit: + self.conn.commit() self.data.set(id, self.FIELD_MAP['isbn'], isbn, row_is_id=True) if notify: self.notify('metadata', [id]) From 99b0d8f2b9ec980e650aacd0aa31d0a95017334a Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 18 Aug 2010 12:24:58 -0600 Subject: [PATCH 59/73] Fix bug in new bulk tag edit function when adding already existing tags --- src/calibre/library/custom_columns.py | 2 +- src/calibre/library/database2.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/library/custom_columns.py b/src/calibre/library/custom_columns.py index f02294a102..60be09a193 100644 --- a/src/calibre/library/custom_columns.py +++ b/src/calibre/library/custom_columns.py @@ -377,7 +377,7 @@ class CustomColumns(object): if add: self.conn.execute( ''' - INSERT INTO {0}(book, value) SELECT {1}.id, {2}.id FROM {1}, {2} + INSERT OR REPLACE INTO {0}(book, value) SELECT {1}.id, {2}.id FROM {1}, {2} '''.format(link_table, temp_tables[0], temp_tables[1]) ) # get rid of the temp tables diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 6728735eb3..4b7a71aa64 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -1434,7 +1434,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): if add: self.conn.execute( ''' - INSERT INTO books_tags_link(book, tag) SELECT {0}.id, {1}.id FROM + INSERT OR REPLACE INTO books_tags_link(book, tag) SELECT {0}.id, {1}.id FROM {0}, {1} '''.format(tables[0], tables[1]) ) From 5d846dae6f85e2447600adb72f3a0dbff2b867f1 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 18 Aug 2010 13:02:14 -0600 Subject: [PATCH 60/73] Fix errors raised during bulk metadata edit causing hang --- src/calibre/gui2/dialogs/metadata_bulk.py | 151 ++++++++++++---------- 1 file changed, 83 insertions(+), 68 deletions(-) diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index c216463941..9b6f8e53ab 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -3,8 +3,9 @@ __copyright__ = '2008, Kovid Goyal ' '''Dialog to edit metadata in bulk''' -from PyQt4.Qt import SIGNAL, QObject, QDialog, QGridLayout, \ - QThread, Qt +from threading import Thread + +from PyQt4.Qt import QDialog, QGridLayout from calibre.gui2.dialogs.metadata_bulk_ui import Ui_MetadataBulkDialog from calibre.gui2.dialogs.tag_editor import TagEditor @@ -12,18 +13,73 @@ from calibre.ebooks.metadata import string_to_authors, \ authors_to_string from calibre.gui2.custom_column_widgets import populate_metadata_page from calibre.gui2.dialogs.progress import BlockingBusy -from calibre.gui2 import error_dialog +from calibre.gui2 import error_dialog, Dispatcher -class Worker(QThread): +class Worker(Thread): - def __init__(self, func, parent=None): - QThread.__init__(self, parent) - self.func = func + def __init__(self, args, db, ids, callback): + Thread.__init__(self) + self.args = args + self.db = db + self.ids = ids self.error = None + self.callback = callback + + def doit(self): + remove, add, au, aus, do_aus, rating, pub, do_series, \ + do_autonumber, do_remove_format, remove_format, do_swap_ta, \ + do_remove_conv, do_auto_author, series = self.args + + for id in self.ids: + if do_swap_ta: + title = self.db.title(id, index_is_id=True) + aum = self.db.authors(id, index_is_id=True) + if aum: + aum = [a.strip().replace('|', ',') for a in aum.split(',')] + new_title = authors_to_string(aum) + self.db.set_title(id, new_title, notify=False) + if title: + new_authors = string_to_authors(title) + self.db.set_authors(id, new_authors, notify=False) + + if au: + self.db.set_authors(id, string_to_authors(au), notify=False) + + if do_auto_author: + x = self.db.author_sort_from_book(id, index_is_id=True) + if x: + self.db.set_author_sort(id, x, notify=False) + + if aus and do_aus: + self.db.set_author_sort(id, aus, notify=False) + + if rating != -1: + self.db.set_rating(id, 2*rating, notify=False) + + if pub: + self.db.set_publisher(id, pub, notify=False) + + if do_series: + next = self.db.get_next_series_num_for(series) + self.db.set_series(id, series, notify=False) + num = next if do_autonumber and series else 1.0 + self.db.set_series_index(id, num, notify=False) + + if do_remove_format: + self.db.remove_format(id, remove_format, index_is_id=True, notify=False) + + if do_remove_conv: + self.db.delete_conversion_options(id, 'PIPE') + + for w in getattr(self, 'custom_column_widgets', []): + w.commit(self.ids) + self.db.bulk_modify_tags(self.ids, add=add, remove=remove, + notify=False) + self.db.clean() def run(self): try: - self.func() + self.doit() except Exception, err: import traceback try: @@ -32,6 +88,9 @@ class Worker(QThread): err = repr(err) self.error = (err, traceback.format_exc()) + self.callback() + + class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): def __init__(self, window, rows, db): @@ -57,9 +116,9 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): self.remove_format.setCurrentIndex(-1) - QObject.connect(self.series, SIGNAL('currentIndexChanged(int)'), self.series_changed) - QObject.connect(self.series, SIGNAL('editTextChanged(QString)'), self.series_changed) - QObject.connect(self.tag_editor_button, SIGNAL('clicked()'), self.tag_editor) + self.series.currentIndexChanged[int].connect(self.series_changed) + self.series.editTextChanged.connect(self.series_changed) + self.tag_editor_button.clicked.connect(self.tag_editor) if len(db.custom_column_label_map) == 0: self.central_widget.tabBar().setVisible(False) else: @@ -113,7 +172,7 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): self.publisher.addItem(name) self.publisher.setEditText('') - def tag_editor(self): + def tag_editor(self, *args): d = TagEditor(self, self.db, None) d.exec_() if d.result() == QDialog.Accepted: @@ -126,6 +185,12 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): if len(self.ids) < 1: return QDialog.accept(self) + self.changed = bool(self.ids) + # Cache values from GUI so that Qt widgets are not used in + # non GUI thread + for w in getattr(self, 'custom_column_widgets', []): + w.gui_val + remove = unicode(self.remove_tags.text()).strip().split(',') add = unicode(self.tags.text()).strip().split(',') au = unicode(self.authors.text()) @@ -141,66 +206,16 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): do_swap_ta = self.swap_title_and_author.isChecked() do_remove_conv = self.remove_conversion_settings.isChecked() do_auto_author = self.auto_author_sort.isChecked() - self.changed = bool(self.ids) - # Cache values from GUI so that Qt widgets are not used in - # non GUI thread - for w in getattr(self, 'custom_column_widgets', []): - w.gui_val - def doit(): - for id in self.ids: - if do_swap_ta: - title = self.db.title(id, index_is_id=True) - aum = self.db.authors(id, index_is_id=True) - if aum: - aum = [a.strip().replace('|', ',') for a in aum.split(',')] - new_title = authors_to_string(aum) - self.db.set_title(id, new_title, notify=False) - if title: - new_authors = string_to_authors(title) - self.db.set_authors(id, new_authors, notify=False) - - if au: - self.db.set_authors(id, string_to_authors(au), notify=False) - - if do_auto_author: - x = self.db.author_sort_from_book(id, index_is_id=True) - if x: - self.db.set_author_sort(id, x, notify=False) - - if aus and do_aus: - self.db.set_author_sort(id, aus, notify=False) - - if rating != -1: - self.db.set_rating(id, 2*rating, notify=False) - - if pub: - self.db.set_publisher(id, pub, notify=False) - - if do_series: - next = self.db.get_next_series_num_for(series) - self.db.set_series(id, series, notify=False) - num = next if do_autonumber and series else 1.0 - self.db.set_series_index(id, num, notify=False) - - if do_remove_format: - self.db.remove_format(id, remove_format, index_is_id=True, notify=False) - - if do_remove_conv: - self.db.delete_conversion_options(id, 'PIPE') - - for w in getattr(self, 'custom_column_widgets', []): - w.commit(self.ids) - self.db.bulk_modify_tags(self.ids, add=add, remove=remove, - notify=False) - self.db.clean() - - self.worker = Worker(doit, self) - self.worker.start() + args = (remove, add, au, aus, do_aus, rating, pub, do_series, + do_autonumber, do_remove_format, remove_format, do_swap_ta, + do_remove_conv, do_auto_author, series) bb = BlockingBusy(_('Applying changes to %d books. This may take a while.') %len(self.ids), parent=self) - self.worker.finished.connect(bb.accept, type=Qt.QueuedConnection) + self.worker = Worker(args, self.db, self.ids, Dispatcher(bb.accept, + parent=bb)) + self.worker.start() bb.exec_() if self.worker.error is not None: From 8a0d3e391303952ed91a2bd5ba98fe1128c25442 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 18 Aug 2010 20:06:31 +0100 Subject: [PATCH 61/73] Commit to accept Kovid's dialog changes --- src/calibre/gui2/dialogs/progress.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/calibre/gui2/dialogs/progress.py b/src/calibre/gui2/dialogs/progress.py index 79b4af5ea4..8bdeb65da3 100644 --- a/src/calibre/gui2/dialogs/progress.py +++ b/src/calibre/gui2/dialogs/progress.py @@ -83,9 +83,9 @@ class BlockingBusy(QDialog): self.font = QFont() self.font.setPointSize(self.font.pointSize() + 8) self.msg.setFont(self.font) -# self.pi = ProgressIndicator(self) -# self.pi.setDisplaySize(100) -# self._layout.addWidget(self.pi, 0, Qt.AlignHCenter) + self.pi = ProgressIndicator(self) + self.pi.setDisplaySize(100) + self._layout.addWidget(self.pi, 0, Qt.AlignHCenter) self._layout.addSpacing(15) self._layout.addWidget(self.msg, 0, Qt.AlignHCenter) self.start() @@ -93,11 +93,11 @@ class BlockingBusy(QDialog): self.resize(self.sizeHint()) def start(self): -# self.pi.startAnimation() + self.pi.startAnimation() pass def stop(self): -# self.pi.stopAnimation() + self.pi.stopAnimation() pass def accept(self): From 3d7f782726d29e7d90c26e25ab3ef628a49d6678 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 18 Aug 2010 13:10:49 -0600 Subject: [PATCH 62/73] ... --- src/calibre/gui2/dialogs/metadata_bulk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index 9b6f8e53ab..b6c5fb278b 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -225,6 +225,6 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): return QDialog.accept(self) - def series_changed(self): + def series_changed(self, *args): self.write_series = True From fafa58adb7a653af535a76bf2633bdd9011e48d9 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 18 Aug 2010 20:22:25 +0100 Subject: [PATCH 63/73] Add single-transaction behavior to bulk metadata edit --- src/calibre/gui2/dialogs/metadata_bulk.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index 9b6f8e53ab..59189838cb 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -30,6 +30,10 @@ class Worker(Thread): do_autonumber, do_remove_format, remove_format, do_swap_ta, \ do_remove_conv, do_auto_author, series = self.args + # first loop: do author and title. These will commit at the end of each + # operation, because each operation modifies the file system. We want to + # try hard to keep the DB and the file system in sync, even in the face + # of exceptions or forced exits. for id in self.ids: if do_swap_ta: title = self.db.title(id, index_is_id=True) @@ -45,31 +49,34 @@ class Worker(Thread): if au: self.db.set_authors(id, string_to_authors(au), notify=False) + # All of these just affect the DB, so we can tolerate a total rollback + for id in self.ids: if do_auto_author: - x = self.db.author_sort_from_book(id, index_is_id=True) + x = self.db.author_sort_from_book(id, index_is_id=True, commit=False) if x: - self.db.set_author_sort(id, x, notify=False) + self.db.set_author_sort(id, x, notify=False, commit=False) if aus and do_aus: - self.db.set_author_sort(id, aus, notify=False) + self.db.set_author_sort(id, aus, notify=False, commit=False) if rating != -1: - self.db.set_rating(id, 2*rating, notify=False) + self.db.set_rating(id, 2*rating, notify=False, commit=False) if pub: - self.db.set_publisher(id, pub, notify=False) + self.db.set_publisher(id, pub, notify=False, commit=False) if do_series: next = self.db.get_next_series_num_for(series) - self.db.set_series(id, series, notify=False) + self.db.set_series(id, series, notify=False, commit=False) num = next if do_autonumber and series else 1.0 - self.db.set_series_index(id, num, notify=False) + self.db.set_series_index(id, num, notify=False, commit=False) if do_remove_format: - self.db.remove_format(id, remove_format, index_is_id=True, notify=False) + self.db.remove_format(id, remove_format, index_is_id=True, notify=False, commit=False) if do_remove_conv: self.db.delete_conversion_options(id, 'PIPE') + self.db.conn.commit() for w in getattr(self, 'custom_column_widgets', []): w.commit(self.ids) From 0521321d691126f05d7e67904da10770a5cba764 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 18 Aug 2010 20:24:04 +0100 Subject: [PATCH 64/73] Remove debugging statements from progress.py --- src/calibre/gui2/dialogs/progress.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/calibre/gui2/dialogs/progress.py b/src/calibre/gui2/dialogs/progress.py index 8bdeb65da3..553ee4b03b 100644 --- a/src/calibre/gui2/dialogs/progress.py +++ b/src/calibre/gui2/dialogs/progress.py @@ -94,11 +94,9 @@ class BlockingBusy(QDialog): def start(self): self.pi.startAnimation() - pass def stop(self): self.pi.stopAnimation() - pass def accept(self): self.stop() From 5aaa7b620c2fc38732da506c53312a21723a963d Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 18 Aug 2010 20:51:24 +0100 Subject: [PATCH 65/73] Make cc editing work with new threading structure. --- src/calibre/gui2/dialogs/metadata_bulk.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index 59189838cb..330c137570 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -77,12 +77,7 @@ class Worker(Thread): if do_remove_conv: self.db.delete_conversion_options(id, 'PIPE') self.db.conn.commit() - - for w in getattr(self, 'custom_column_widgets', []): - w.commit(self.ids) - self.db.bulk_modify_tags(self.ids, add=add, remove=remove, - notify=False) - self.db.clean() + self.db.bulk_modify_tags(self.ids, add=add, remove=remove, notify=False) def run(self): try: @@ -229,6 +224,11 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): return error_dialog(self, _('Failed'), self.worker.error[0], det_msg=self.worker.error[1], show=True) + + for w in getattr(self, 'custom_column_widgets', []): + w.commit(self.ids) + self.db.clean() + return QDialog.accept(self) From 9f223175f8374d745847e0a123c944be6c402bd6 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 18 Aug 2010 14:01:25 -0600 Subject: [PATCH 66/73] Fix bulk editing of customn column widgets broken --- src/calibre/gui2/dialogs/metadata_bulk.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index b6c5fb278b..3c03b21783 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -17,13 +17,14 @@ from calibre.gui2 import error_dialog, Dispatcher class Worker(Thread): - def __init__(self, args, db, ids, callback): + def __init__(self, args, db, ids, cc_widgets, callback): Thread.__init__(self) self.args = args self.db = db self.ids = ids self.error = None self.callback = callback + self.cc_widgets = cc_widgets def doit(self): remove, add, au, aus, do_aus, rating, pub, do_series, \ @@ -71,7 +72,7 @@ class Worker(Thread): if do_remove_conv: self.db.delete_conversion_options(id, 'PIPE') - for w in getattr(self, 'custom_column_widgets', []): + for w in self.cc_widgets: w.commit(self.ids) self.db.bulk_modify_tags(self.ids, add=add, remove=remove, notify=False) @@ -213,8 +214,9 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): bb = BlockingBusy(_('Applying changes to %d books. This may take a while.') %len(self.ids), parent=self) - self.worker = Worker(args, self.db, self.ids, Dispatcher(bb.accept, - parent=bb)) + self.worker = Worker(args, self.db, self.ids, + getattr(self, 'custom_column_widgets', []), + Dispatcher(bb.accept, parent=bb)) self.worker.start() bb.exec_() From 62eca8531635fd4ad25708d8039b8a8cbf4d53c0 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 18 Aug 2010 21:30:56 +0100 Subject: [PATCH 67/73] Make metadata single use fewer transactions --- src/calibre/gui2/custom_column_widgets.py | 4 +-- src/calibre/gui2/dialogs/metadata_single.py | 38 +++++++++++++-------- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/src/calibre/gui2/custom_column_widgets.py b/src/calibre/gui2/custom_column_widgets.py index 4a1a85f159..f6ed1e4d6f 100644 --- a/src/calibre/gui2/custom_column_widgets.py +++ b/src/calibre/gui2/custom_column_widgets.py @@ -41,7 +41,7 @@ class Base(object): val = self.gui_val val = self.normalize_ui_val(val) if val != self.initial_val: - self.db.set_custom(book_id, val, num=self.col_id, notify=notify) + self.db._set_custom(book_id, val, num=self.col_id, notify=notify) def normalize_db_val(self, val): return val @@ -304,7 +304,7 @@ class Series(Base): num=self.col_id) else: s_index = None - self.db.set_custom(book_id, val, extra=s_index, + self.db._set_custom(book_id, val, extra=s_index, num=self.col_id, notify=notify) widgets = { diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py index 50223923c1..9780f84c61 100644 --- a/src/calibre/gui2/dialogs/metadata_single.py +++ b/src/calibre/gui2/dialogs/metadata_single.py @@ -668,9 +668,9 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): self.tags.setText(', '.join(book.tags)) if book.series is not None: if self.series.text() is None or self.series.text() == '': - self.series.setText(book.series) - if book.series_index is not None: - self.series_index.setValue(book.series_index) + self.series.setText(book.series) + if book.series_index is not None: + self.series_index.setValue(book.series_index) else: error_dialog(self, _('Cannot fetch metadata'), _('You must specify at least one of ISBN, Title, ' @@ -719,24 +719,31 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): self.db.set_authors(self.id, string_to_authors(au), notify=False) aus = unicode(self.author_sort.text()) if aus: - self.db.set_author_sort(self.id, aus, notify=False) + self.db.set_author_sort(self.id, aus, notify=False, commit=False) self.db.set_isbn(self.id, - re.sub(r'[^0-9a-zA-Z]', '', unicode(self.isbn.text())), notify=False) - self.db.set_rating(self.id, 2*self.rating.value(), notify=False) - self.db.set_publisher(self.id, unicode(self.publisher.currentText()), notify=False) + re.sub(r'[^0-9a-zA-Z]', '', unicode(self.isbn.text())), + notify=False, commit=False) + self.db.set_rating(self.id, 2*self.rating.value(), notify=False, + commit=False) + self.db.set_publisher(self.id, unicode(self.publisher.currentText()), + notify=False, commit=False) self.db.set_tags(self.id, [x.strip() for x in - unicode(self.tags.text()).split(',')], notify=False) + unicode(self.tags.text()).split(',')], notify=False, commit=False) self.db.set_series(self.id, - unicode(self.series.currentText()).strip(), notify=False) - self.db.set_series_index(self.id, self.series_index.value(), notify=False) - self.db.set_comment(self.id, unicode(self.comments.toPlainText()), notify=False) + unicode(self.series.currentText()).strip(), notify=False, + commit=False) + self.db.set_series_index(self.id, self.series_index.value(), + notify=False, commit=False) + self.db.set_comment(self.id, unicode(self.comments.toPlainText()), + notify=False, commit=False) d = self.pubdate.date() d = qt_to_dt(d) - self.db.set_pubdate(self.id, d, notify=False) + self.db.set_pubdate(self.id, d, notify=False, commit=False) d = self.date.date() d = qt_to_dt(d) if d.date() != self.orig_timestamp.date(): - self.db.set_timestamp(self.id, d, notify=False) + self.db.set_timestamp(self.id, d, notify=False, commit=False) + self.db.conn.commit() if self.cover_changed: if self.cover_data is not None: @@ -745,6 +752,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): self.db.remove_cover(self.id) for w in getattr(self, 'custom_column_widgets', []): w.commit(self.id) + self.db.conn.commit() except IOError, err: if err.errno == 13: # Permission denied fname = err.filename if err.filename else 'file' @@ -769,9 +777,9 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): wg = dynamic.get('metasingle_window_geometry', None) ss = dynamic.get('metasingle_splitter_state', None) if wg is not None: - self.restoreGeometry(wg) + self.restoreGeometry(wg) if ss is not None: - self.splitter.restoreState(ss) + self.splitter.restoreState(ss) def save_state(self): dynamic.set('metasingle_window_geometry', bytes(self.saveGeometry())) From c334e84b48a67586edcb22893335b35fe5dd913d Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 18 Aug 2010 21:35:08 +0100 Subject: [PATCH 68/73] Change metadata single to use db.commit instead of conn.commit --- src/calibre/gui2/dialogs/metadata_single.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py index 9780f84c61..3e07422967 100644 --- a/src/calibre/gui2/dialogs/metadata_single.py +++ b/src/calibre/gui2/dialogs/metadata_single.py @@ -743,7 +743,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): d = qt_to_dt(d) if d.date() != self.orig_timestamp.date(): self.db.set_timestamp(self.id, d, notify=False, commit=False) - self.db.conn.commit() + self.db.commit() if self.cover_changed: if self.cover_data is not None: @@ -752,7 +752,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): self.db.remove_cover(self.id) for w in getattr(self, 'custom_column_widgets', []): w.commit(self.id) - self.db.conn.commit() + self.db.commit() except IOError, err: if err.errno == 13: # Permission denied fname = err.filename if err.filename else 'file' From c29ee263e0f41dc4a4f6cbe6a1e526a6d0b0b95f Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 18 Aug 2010 21:38:22 +0100 Subject: [PATCH 69/73] Add commit= parameter to set custom. Use it in cc_widgets --- src/calibre/gui2/custom_column_widgets.py | 7 ++++--- src/calibre/library/custom_columns.py | 5 +++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/calibre/gui2/custom_column_widgets.py b/src/calibre/gui2/custom_column_widgets.py index f6ed1e4d6f..f364638f37 100644 --- a/src/calibre/gui2/custom_column_widgets.py +++ b/src/calibre/gui2/custom_column_widgets.py @@ -41,7 +41,8 @@ class Base(object): val = self.gui_val val = self.normalize_ui_val(val) if val != self.initial_val: - self.db._set_custom(book_id, val, num=self.col_id, notify=notify) + self.db.set_custom(book_id, val, num=self.col_id, notify=notify, + commit=False) def normalize_db_val(self, val): return val @@ -304,8 +305,8 @@ class Series(Base): num=self.col_id) else: s_index = None - self.db._set_custom(book_id, val, extra=s_index, - num=self.col_id, notify=notify) + self.db.set_custom(book_id, val, extra=s_index, + num=self.col_id, notify=notify, commit=False) widgets = { 'bool' : Bool, diff --git a/src/calibre/library/custom_columns.py b/src/calibre/library/custom_columns.py index 60be09a193..4ba664dadc 100644 --- a/src/calibre/library/custom_columns.py +++ b/src/calibre/library/custom_columns.py @@ -412,10 +412,11 @@ class CustomColumns(object): self.conn.commit() def set_custom(self, id, val, label=None, num=None, - append=False, notify=True, extra=None): + append=False, notify=True, extra=None, commit=True): self._set_custom(id, val, label=label, num=num, append=append, notify=notify, extra=extra) - self.conn.commit() + if commit: + self.conn.commit() def _set_custom(self, id_, val, label=None, num=None, append=False, notify=True, extra=None): From 6eee6497f01974dfe351d7f2d1e85ee3938a6311 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 18 Aug 2010 21:41:52 +0100 Subject: [PATCH 70/73] Remove redundant calls to cc widget commits --- src/calibre/gui2/dialogs/metadata_bulk.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index b53060c3d3..fc0a0c5840 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -231,11 +231,6 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): return error_dialog(self, _('Failed'), self.worker.error[0], det_msg=self.worker.error[1], show=True) - - for w in getattr(self, 'custom_column_widgets', []): - w.commit(self.ids) - self.db.clean() - return QDialog.accept(self) From 8b452ac88be6231b28e7aa49d2572564df05bbd2 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 18 Aug 2010 22:42:56 +0100 Subject: [PATCH 71/73] Fix regression in content server - exception raised when starting from the GUI because the restriction option isn't there. --- src/calibre/library/server/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/library/server/base.py b/src/calibre/library/server/base.py index 7f808b008c..e949c712d4 100644 --- a/src/calibre/library/server/base.py +++ b/src/calibre/library/server/base.py @@ -95,8 +95,8 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache): 'tools.digest_auth.users' : {opts.username.strip():opts.password.strip()}, } - sr = db.prefs.get('cs_restriction', '') if opts.restriction is None \ - else opts.restriction + sr = getattr(opts, 'restriction', None) + sr = db.prefs.get('cs_restriction', '') if sr is None else sr self.set_search_restriction(sr) self.is_running = False From e897b727342410226ff8c42b579da27ba5d925cd Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 18 Aug 2010 15:54:31 -0600 Subject: [PATCH 72/73] Driver for the Spectra --- src/calibre/customize/builtins.py | 3 ++- src/calibre/devices/hanlin/driver.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 35d7c846d0..7bdeaef481 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -438,7 +438,7 @@ from calibre.ebooks.txt.output import TXTOutput from calibre.customize.profiles import input_profiles, output_profiles from calibre.devices.apple.driver import ITUNES -from calibre.devices.hanlin.driver import HANLINV3, HANLINV5, BOOX +from calibre.devices.hanlin.driver import HANLINV3, HANLINV5, BOOX, SPECTRA from calibre.devices.blackberry.driver import BLACKBERRY from calibre.devices.cybook.driver import CYBOOK from calibre.devices.eb600.driver import EB600, COOL_ER, SHINEBOOK, \ @@ -567,6 +567,7 @@ plugins += [ MENTOR, SWEEX, PDNOVEL, + SPECTRA, ITUNES, ] plugins += [x for x in list(locals().values()) if isinstance(x, type) and \ diff --git a/src/calibre/devices/hanlin/driver.py b/src/calibre/devices/hanlin/driver.py index 10dd333b8d..3fbec66987 100644 --- a/src/calibre/devices/hanlin/driver.py +++ b/src/calibre/devices/hanlin/driver.py @@ -88,6 +88,8 @@ class SPECTRA(HANLINV3): FORMATS = ['epub', 'mobi', 'fb2', 'lit', 'prc', 'djvu', 'pdf', 'rtf', 'txt'] + SUPPORTS_SUB_DIRS = True + class HANLINV5(HANLINV3): name = 'Hanlin V5 driver' gui_name = 'Hanlin V5' From 27fe778e0fa9cf3a2dac8d11bfd7e590658b8015 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 18 Aug 2010 22:58:30 +0100 Subject: [PATCH 73/73] Fix problem where typing in the search box does not clear the saved_search combo box. --- src/calibre/gui2/search_box.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/calibre/gui2/search_box.py b/src/calibre/gui2/search_box.py index 7169eb5fd3..8986346dbc 100644 --- a/src/calibre/gui2/search_box.py +++ b/src/calibre/gui2/search_box.py @@ -373,6 +373,7 @@ class SearchBoxMixin(object): self.set_number_of_books_shown() def search_box_changed(self): + self.saved_search.clear_to_help() self.tags_view.clear() def do_advanced_search(self, *args):