Example #1
0
def icon_map():
    global _icon_map
    with _icon_map_lock:
        if _icon_map is None:
            _icon_map = category_icon_map.copy()
            custom_icons = JSONConfig('gui').get('tags_browser_category_icons', {})
            for k, v in custom_icons.iteritems():
                if os.access(os.path.join(config_dir, 'tb_icons', v), os.R_OK):
                    _icon_map[k] = '_' + quote(v)
            _icon_map['file_type_icons'] = {
                k:'mimetypes/%s.png' % v for k, v in EXT_MAP.iteritems()
            }
        return _icon_map
Example #2
0
    def __init__(self, parent=None, config_name='shortcuts/main'):
        QObject.__init__(self, parent)

        self.config = JSONConfig(config_name)
        self.shortcuts = OrderedDict()
        self.keys_map = {}
        self.groups = {}
    def __init__(self, *args, **kwargs):
        super(DJVUmaker, self).__init__(*args, **kwargs)
        self.prints = prints # Easer access because of Calibre load plugins instead of importing
        # Set default preferences for JSONConfig
        DEFAULT_STORE_VALUES = {}
        DEFAULT_STORE_VALUES['plugin_version'] = PLUGINVER
        DEFAULT_STORE_VALUES['postimport'] = False
        for item in self.REGISTERED_BACKENDS:
            DEFAULT_STORE_VALUES[item] = {
                'flags' : [], 'installed' : False, 'version' : None}
        if 'djvudigital' in self.REGISTERED_BACKENDS:
            DEFAULT_STORE_VALUES['use_backend'] = 'djvudigital'
        else:
            raise Exception('No djvudigital backend.')

        # JSONConfig is a dict-like object,
        # if coresponding .json file has not a specific key, it's got from .defaults
        self.plugin_prefs = JSONConfig(os.path.join('plugins', PLUGINNAME))
        self.plugin_prefs.defaults = DEFAULT_STORE_VALUES

        # make sure to create plugins/djvumaker.json
        # self.plugin_prefs.values() doesn't use self.plugin_prefs.__getitem__()
        # and returns real json, not defaults
        if not self.plugin_prefs.values():
            for key, val in DEFAULT_STORE_VALUES.iteritems():
                self.plugin_prefs[key] = val
Example #4
0
 def cache(self):
     if not hasattr(self, '_mr_cache'):
         from calibre.utils.config import JSONConfig
         self._mr_cache = JSONConfig('mobileread_get_books')
         self._mr_cache.file_path = os.path.join(cache_dir(),
                                              'mobileread_get_books.json')
         self._mr_cache.refresh()
     return self._mr_cache
    def __init__(self, database, book_id, connections):
        self._connections = connections

        book_path = database.field_for('path', book_id).replace('/', os.sep)

        self._prefs = JSONConfig(os.path.join(book_path, 'book_settings'), base_path=LIBRARY)
        self._prefs.setdefault('asin', '')
        self._prefs.setdefault('goodreads_url', '')
        self._prefs.setdefault('aliases', {})
        self._prefs.setdefault('sample_xray', '')
        self._prefs.commit()

        self._title = database.field_for('title', book_id)
        self._author = ' & '.join(database.field_for('authors', book_id))

        self._asin = self._prefs['asin'] if self._prefs['asin'] != '' else None
        self._goodreads_url = self._prefs['goodreads_url']
        self._sample_xray = self._prefs['sample_xray']

        if not self._asin:
            identifiers = database.field_for('identifiers', book_id)
            if 'mobi-asin' in identifiers.keys():
                self._asin = database.field_for('identifiers', book_id)['mobi-asin'].decode('ascii')
                self._prefs['asin'] = self._asin
            else:
                self._asin = self.search_for_asin_on_amazon(self.title_and_author)
                if self._asin:
                    metadata = database.get_metadata(book_id)
                    identifiers = metadata.get_identifiers()
                    identifiers['mobi-asin'] = self._asin
                    metadata.set_identifiers(identifiers)
                    database.set_metadata(book_id, metadata)
                    self._prefs['asin'] = self._asin

        if self._goodreads_url == '':
            url = None
            if self._asin:
                url = self.search_for_goodreads_url(self._asin)
            if not url and self._title != 'Unknown' and self._author != 'Unknown':
                url = self.search_for_goodreads_url(self.title_and_author)

            if url:
                self._goodreads_url = url
                self._prefs['goodreads_url'] = self._goodreads_url
                if not self._asin:
                    self._asin = self.search_for_asin_on_goodreads(self._goodreads_url)
                    if self._asin:
                        metadata = database.get_metadata(book_id)
                        identifiers = metadata.get_identifiers()
                        identifiers['mobi-asin'] = self._asin
                        metadata.set_identifiers(identifiers)
                        database.set_metadata(book_id, metadata)
                        self._prefs['asin'] = self._asin

        self._aliases = self._prefs['aliases']

        self.save()
Example #6
0
 def reload_cache(self):
     if not hasattr(self, 'cache'):
         from calibre.utils.config import JSONConfig
         self.cache = JSONConfig('fonts/scanner_cache')
     else:
         self.cache.refresh()
     if self.cache.get('version', None) != self.CACHE_VERSION:
         self.cache.clear()
     self.cached_fonts = self.cache.get('fonts', {})
Example #7
0
 def builtins_loaded(self):
     self.last_check_time = 0
     self.version_map = {}
     self.cached_version_map = {}
     self.name_rmap = {}
     for key, val in self.iteritems():
         prefix, name = val.__module__.rpartition('.')[0::2]
         if prefix == 'calibre.gui2.store.stores' and name.endswith('_plugin'):
             module = sys.modules[val.__module__]
             sv = getattr(module, 'store_version', None)
             if sv is not None:
                 name = name.rpartition('_')[0]
                 self.version_map[name] = sv
                 self.name_rmap[name] = key
     self.cache_file = JSONConfig('store/plugin_cache')
     self.load_cache()
    def __init__(self, db, book_id, aConnection, sConnection):
        self._db = db
        self._book_id = book_id
        self._aConnection = aConnection
        self._sConnection = sConnection

        book_path = self._db.field_for('path', book_id).replace('/', os.sep)

        self._prefs = JSONConfig(os.path.join(book_path, 'book_settings'), base_path=self.LIBRARY)
        self._prefs.setdefault('asin', '')
        self._prefs.setdefault('shelfari_url', '')
        self._prefs.setdefault('aliases', {})
        self._prefs.commit()

        self._title = self._db.field_for('title', book_id)
        self._author = ' & '.join(self._db.field_for('authors', self._book_id))

        self.asin = self._prefs['asin']
        if self.asin == '':
            identifiers = self._db.field_for('identifiers', self._book_id)
            self.asin = self._db.field_for('identifiers', self._book_id)['mobi-asin'].decode('ascii') if 'mobi-asin' in identifiers.keys() else None
            if not self.asin:
                self.asin = self.get_asin()
            if self.asin:
                self._prefs['asin'] = self.asin

        self.shelfari_url = self._prefs['shelfari_url']
        if self.shelfari_url == '':
            url = ''
            if self._prefs['asin'] != '':
                url = self.search_shelfari(self._prefs['asin'])
            if url != '' and self.title != 'Unknown' and self.author != 'Unknown':
                url = self.search_shelfari(self.title_and_author)

            if url != '':
                self.shelfari_url = url
                self._prefs['shelfari_url'] = self.shelfari_url

        self._aliases = self._prefs['aliases']
        if len(self._aliases.keys()) == 0 and self.shelfari_url != '':
            self.update_aliases()
        self.save()
Example #9
0
from PyQt5.Qt import (QImage, Qt, QFont, QPainter, QPointF, QTextLayout,
                      QTextOption, QFontMetrics, QTextCharFormat, QColor,
                      QRect, QBrush, QLinearGradient, QPainterPath, QPen,
                      QRectF, QTransform, QRadialGradient)

from calibre import force_unicode, fit_image
from calibre.constants import __appname__, __version__
from calibre.ebooks.metadata import fmt_sidx
from calibre.ebooks.metadata.book.base import Metadata
from calibre.ebooks.metadata.book.formatter import SafeFormat
from calibre.gui2 import ensure_app, config, load_builtin_fonts, pixmap_to_data
from calibre.utils.cleantext import clean_ascii_chars, clean_xml_chars
from calibre.utils.config import JSONConfig

# Default settings {{{
cprefs = JSONConfig('cover_generation')
cprefs.defaults['title_font_size'] = 120  # px
cprefs.defaults['subtitle_font_size'] = 80  # px
cprefs.defaults['footer_font_size'] = 80  # px
cprefs.defaults['cover_width'] = 1200  # px
cprefs.defaults['cover_height'] = 1600  # px
cprefs.defaults['title_font_family'] = None
cprefs.defaults['subtitle_font_family'] = None
cprefs.defaults['footer_font_family'] = None
cprefs.defaults['color_themes'] = {}
cprefs.defaults['disabled_color_themes'] = []
cprefs.defaults['disabled_styles'] = []
cprefs.defaults['title_template'] = '<b>{title}'
cprefs.defaults[
    'subtitle_template'] = '''{series:'test($, strcat("<i>", $, "</i> - ", raw_field("formatted_series_index")), "")'}'''
cprefs.defaults['footer_template'] = r'''program:
Example #10
0
from __future__ import (unicode_literals, division, absolute_import, print_function)

__license__   = 'GPL v3'
__copyright__ = '2013, 2014, Jellby <*****@*****.**>'
__docformat__ = 'restructuredtext en'

try:
    from PyQt5.Qt import Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit, QPushButton, QFileDialog, QCheckBox, QComboBox, QGroupBox
except ImportError:
    from PyQt4.Qt import Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit, QPushButton, QFileDialog, QCheckBox, QComboBox, QGroupBox
from calibre.utils.config import JSONConfig
from calibre_plugins.prince_pdf.texteditwithtooltip import TextEditWithTooltip

load_translations()

prefs = JSONConfig('plugins/prince_pdf')

# Set defaults
prefs.defaults['prince_exe'] = 'prince'
prefs.defaults['formats'] = ['epub','azw3','htmlz']
prefs.defaults['add_book'] = True
prefs.defaults['show_CSS'] = False
prefs.defaults['default_CSS'] = _('default')
prefs.defaults['custom_CSS_list'] = {_('default'):'''/*
@font-face {
  font-family: serif;
  src: local("Droid Serif")
}
*/

@page {
Example #11
0
from calibre import sanitize_file_name
from calibre.ebooks.conversion.plugins.pdf_output import PAPER_SIZES
from calibre.gui2 import (
    Application, choose_save_file, dynamic, elided_text, error_dialog,
    open_local_file
)
from calibre.gui2.widgets import PaperSizes
from calibre.gui2.widgets2 import Dialog
from calibre.ptempfile import PersistentTemporaryFile
from calibre.utils.config import JSONConfig
from calibre.utils.filenames import expanduser
from calibre.utils.ipc.simple_worker import start_pipe_worker
from calibre.utils.serialize import msgpack_dumps, msgpack_loads


vprefs = JSONConfig('viewer')


class PrintDialog(Dialog):

    OUTPUT_NAME = 'print-to-pdf-choose-file'

    def __init__(self, book_title, parent=None, prefs=vprefs):
        self.book_title = book_title
        self.default_file_name = sanitize_file_name(book_title[:75] + '.pdf')
        self.paper_size_map = {a:getattr(QPageSize, a.capitalize()) for a in PAPER_SIZES}
        Dialog.__init__(self, _('Print to PDF'), 'print-to-pdf', prefs=prefs, parent=parent)

    def setup_ui(self):
        self.vl = vl = QVBoxLayout(self)
        self.l = l = QFormLayout()
class BookSettings(object):
    '''Holds book specific settings'''

    def __init__(self, database, book_id, connections):
        self._connections = connections

        book_path = database.field_for('path', book_id).replace('/', os.sep)

        self._prefs = JSONConfig(os.path.join(book_path, 'book_settings'), base_path=LIBRARY)
        self._prefs.setdefault('asin', '')
        self._prefs.setdefault('goodreads_url', '')
        self._prefs.setdefault('aliases', {})
        self._prefs.setdefault('sample_xray', '')
        self._prefs.commit()

        self._title = database.field_for('title', book_id)
        self._author = ' & '.join(database.field_for('authors', book_id))

        self._asin = self._prefs['asin'] if self._prefs['asin'] != '' else None
        self._goodreads_url = self._prefs['goodreads_url']
        self._sample_xray = self._prefs['sample_xray']

        if not self._asin:
            identifiers = database.field_for('identifiers', book_id)
            if 'mobi-asin' in identifiers.keys():
                self._asin = database.field_for('identifiers', book_id)['mobi-asin'].decode('ascii')
                self._prefs['asin'] = self._asin
            else:
                self._asin = self.search_for_asin_on_amazon(self.title_and_author)
                if self._asin:
                    metadata = database.get_metadata(book_id)
                    identifiers = metadata.get_identifiers()
                    identifiers['mobi-asin'] = self._asin
                    metadata.set_identifiers(identifiers)
                    database.set_metadata(book_id, metadata)
                    self._prefs['asin'] = self._asin

        if self._goodreads_url == '':
            url = None
            if self._asin:
                url = self.search_for_goodreads_url(self._asin)
            if not url and self._title != 'Unknown' and self._author != 'Unknown':
                url = self.search_for_goodreads_url(self.title_and_author)

            if url:
                self._goodreads_url = url
                self._prefs['goodreads_url'] = self._goodreads_url
                if not self._asin:
                    self._asin = self.search_for_asin_on_goodreads(self._goodreads_url)
                    if self._asin:
                        metadata = database.get_metadata(book_id)
                        identifiers = metadata.get_identifiers()
                        identifiers['mobi-asin'] = self._asin
                        metadata.set_identifiers(identifiers)
                        database.set_metadata(book_id, metadata)
                        self._prefs['asin'] = self._asin

        self._aliases = self._prefs['aliases']

        self.save()

    @property
    def prefs(self):
        return self._prefs

    @property
    def asin(self):
        return self._asin

    @asin.setter
    def asin(self, val):
        self._asin = val

    @property
    def sample_xray(self):
        return self._sample_xray

    @sample_xray.setter
    def sample_xray(self, value):
        self._sample_xray = value

    @property
    def title(self):
        return self._title

    @property
    def author(self):
        return self._author

    @property
    def title_and_author(self):
        return '{0} - {1}'.format(self._title, self._author)

    @property
    def goodreads_url(self):
        return self._goodreads_url

    @goodreads_url.setter
    def goodreads_url(self, val):
        self._goodreads_url = val

    @property
    def aliases(self):
        return self._aliases

    def set_aliases(self, label, aliases):
        '''Sets label's aliases to aliases'''

        # 'aliases' is a string containing a comma separated list of aliases.

        # Split it, remove whitespace from each element, drop empty strings (strangely,
        # split only does this if you don't specify a separator)

        # so "" -> []  "foo,bar" and " foo   , bar " -> ["foo", "bar"]
        aliases = [x.strip() for x in aliases.split(",") if x.strip()]
        self._aliases[label] = aliases

    def save(self):
        '''Saves current settings in book's settings file'''
        self._prefs['asin'] = self._asin
        self._prefs['goodreads_url'] = self._goodreads_url
        self._prefs['aliases'] = self._aliases
        self._prefs['sample_xray'] = self._sample_xray

    def search_for_asin_on_amazon(self, query):
        '''Search for book's asin on amazon using given query'''
        query = urlencode({'keywords': query})
        url = '/s/ref=sr_qz_back?sf=qz&rh=i%3Adigital-text%2Cn%3A154606011%2Ck%3A' + query[9:] + '&' + query
        try:
            response = open_url(self._connections['amazon'], url)
        except PageDoesNotExist:
            return None

        # check to make sure there are results
        if ('did not match any products' in response and 'Did you mean:' not in response and
                'so we searched in All Departments' not in response):
            return None

        soup = BeautifulSoup(response)
        results = soup.findAll('div', {'id': 'resultsCol'})

        if not results:
            return None

        for result in results:
            if 'Buy now with 1-Click' in str(result):
                asin_search = AMAZON_ASIN_PAT.search(str(result))
                if asin_search:
                    return asin_search.group(1)

        return None

    def search_for_goodreads_url(self, keywords):
        '''Searches for book's goodreads url using given keywords'''
        query = urlencode({'q': keywords})
        try:
            response = open_url(self._connections['goodreads'], '/search?' + query)
        except PageDoesNotExist:
            return None

        # check to make sure there are results
        if 'No results' in response:
            return None

        urlsearch = GOODREADS_URL_PAT.search(response)
        if not urlsearch:
            return None

        # return the full URL with the query parameters removed
        url = 'https://www.goodreads.com' + urlsearch.group(1)
        return urlparse.urlparse(url)._replace(query=None).geturl()

    def search_for_asin_on_goodreads(self, url):
        '''Searches for ASIN of book at given url'''
        book_id_search = BOOK_ID_PAT.search(url)
        if not book_id_search:
            return None

        book_id = book_id_search.group(1)

        try:
            response = open_url(self._connections['goodreads'], '/buttons/glide/' + book_id)
        except PageDoesNotExist:
            return None

        book_asin_search = GOODREADS_ASIN_PAT.search(response)
        if not book_asin_search:
            return None

        return book_asin_search.group(1)

    def update_aliases(self, source, source_type='url'):
        if source_type.lower() == 'url':
            self.update_aliases_from_url(source)
            return
        if source_type.lower() == 'asc':
            self.update_aliases_from_asc(source)
            return
        if source_type.lower() == 'json':
            self.update_aliases_from_json(source)

    def update_aliases_from_asc(self, filename):
        '''Gets aliases from sample x-ray file and expands them if users settings say to do so'''
        cursor = connect(filename).cursor()
        characters = {x[1]: [x[1]] for x in cursor.execute('SELECT * FROM entity').fetchall() if x[3] == 1}

        self._aliases = {}
        for alias, fullname in auto_expand_aliases(characters).items():
            if fullname not in self._aliases.keys():
                self._aliases[fullname] = [alias]
                continue
            self._aliases[fullname].append(alias)

    def update_aliases_from_json(self, filename):
        '''Gets aliases from json file'''
        data = json.load(open(filename))
        self._aliases = {name: char['aliases'] for name, char in data['characters'].items()} if 'characters' in data else {}
        if 'settings' in data:
            self._aliases.update({name: setting['aliases'] for name, setting in data['settings'].items()})

    def update_aliases_from_url(self, url):
        '''Gets aliases from Goodreads and expands them if users settings say to do so'''
        try:
            goodreads_parser = GoodreadsParser(url, self._connections['goodreads'], self._asin)
            goodreads_chars = goodreads_parser.get_characters(1)
            goodreads_settings = goodreads_parser.get_settings(len(goodreads_chars))
        except PageDoesNotExist:
            goodreads_chars = {}
            goodreads_settings = {}

        self._aliases = {}
        for char_data in goodreads_chars.values() + goodreads_settings.values():
            if char_data['label'] not in self._aliases.keys():
                self._aliases[char_data['label']] = char_data['aliases']
                'Paranormal > Vampires': ['Vampires'],
                'War': ['War'],
                'Western': ['Western'],
                'Language > Writing': ['Writing'],
                'Writing > Essays': ['Writing'],
                'Young Adult': ['Young Adult'],
                }

DEFAULT_STORE_VALUES = {
    KEY_GET_EDITIONS: False,
    KEY_GET_ALL_AUTHORS: False,
    KEY_GENRE_MAPPINGS: copy.deepcopy(DEFAULT_GENRE_MAPPINGS)
}

# This is where all preferences for this plugin will be stored
plugin_prefs = JSONConfig('plugins/Shelfari')

# Set defaults
plugin_prefs.defaults[STORE_NAME] = DEFAULT_STORE_VALUES


class GenreTagMappingsTableWidget(QTableWidget):
    def __init__(self, parent, all_tags):
        QTableWidget.__init__(self, parent)
        self.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.tags_values = all_tags

    def populate_table(self, tag_mappings):
        self.clear()
        self.setAlternatingRowColors(True)
        self.setRowCount(len(tag_mappings))
Example #14
0
'''
Configuration for the Comicvine metadata source
'''
from PyQt4.Qt import QWidget, QHBoxLayout, QLabel, QLineEdit
from calibre.utils.config import JSONConfig

try:
  import pycomicvine #pylint: disable=F0401
except ImportError:
  from calibre_plugins.comicvine import pycomicvine_dist as pycomicvine

PREFS = JSONConfig('plugins/comicvine')
PREFS.defaults['api_key'] = ''
PREFS.defaults['worker_threads'] = '16'

pycomicvine.api_key = PREFS['api_key']

class ConfigWidget(QWidget):
  'Configuration widget'
  def __init__(self):
    QWidget.__init__(self)
    self.layout = QHBoxLayout()
    self.setLayout(self.layout)

    self.key_label = QLabel('&api key:')
    self.layout.addWidget(self.key_label)

    self.key_msg = QLineEdit(self)
    self.key_msg.setText(PREFS['api_key'])
    self.layout.addWidget(self.key_msg)
    self.key_label.setBuddy(self.key_msg)
Example #15
0
import cPickle, os, glob, shutil
from collections import namedtuple
from operator import attrgetter
from itertools import chain

from calibre.constants import plugins, config_dir
from calibre.utils.config import JSONConfig
from calibre.utils.localization import get_lang, canonicalize_lang, get_system_locale

DictionaryLocale = namedtuple("DictionaryLocale", "langcode countrycode")
Dictionary = namedtuple("Dictionary", "primary_locale locales dicpath affpath builtin name id")
LoadedDictionary = namedtuple("Dictionary", "primary_locale locales obj builtin name id")
hunspell = plugins["hunspell"][0]
if hunspell is None:
    raise RuntimeError("Failed to load hunspell: %s" % plugins[1])
dprefs = JSONConfig("dictionaries/prefs.json")
dprefs.defaults["preferred_dictionaries"] = {}
dprefs.defaults["preferred_locales"] = {}
dprefs.defaults["user_dictionaries"] = [{"name": _("Default"), "is_active": True, "words": []}]
not_present = object()


class UserDictionary(object):

    __slots__ = ("name", "is_active", "words")

    def __init__(self, **kwargs):
        self.name = kwargs["name"]
        self.is_active = kwargs["is_active"]
        self.words = {(w, langcode) for w, langcode in kwargs["words"]}
Example #16
0
 def __init__(self, *args, **kw):
     # This has to be loaded on instantiation as it can be shared by
     # multiple processes
     self.PREFS_OBJECT = JSONConfig(self.PREFS_OBJECT_NAME)
     RulesDialogBase.__init__(self, *args, **kw)
Example #17
0
                }

UPDATE_OPTIONS_DEFAULTS = {
                KEY_DO_UPDATE_CHECK: False,
                KEY_LAST_FIRMWARE_CHECK_TIME: 0,
                KEY_DO_EARLY_FIRMWARE_CHECK: False
                }

BACKUP_OPTIONS_DEFAULTS = {
                KEY_DO_DAILY_BACKUP:        False,
                KEY_BACKUP_COPIES_TO_KEEP:  5,
                KEY_BACKUP_DEST_DIRECTORY:  ''
                }

# This is where all preferences for this plugin will be stored
plugin_prefs = JSONConfig('plugins/Sony Utilities')

# Set defaults
plugin_prefs.defaults[BOOKMARK_OPTIONS_STORE_NAME]      = BOOKMARK_OPTIONS_DEFAULTS
plugin_prefs.defaults[METADATA_OPTIONS_STORE_NAME]      = METADATA_OPTIONS_DEFAULTS
plugin_prefs.defaults[READING_OPTIONS_STORE_NAME]       = READING_OPTIONS_DEFAULTS
plugin_prefs.defaults[COMMON_OPTIONS_STORE_NAME]        = COMMON_OPTIONS_DEFAULTS
plugin_prefs.defaults[DISMISSTILES_OPTIONS_STORE_NAME]  = DISMISSTILES_OPTIONS_DEFAULTS
plugin_prefs.defaults[FIXDUPLICATESHELVES_OPTIONS_STORE_NAME]  = FIXDUPLICATESHELVES_OPTIONS_DEFAULTS
plugin_prefs.defaults[ORDERSERIESSHELVES_OPTIONS_STORE_NAME]   = ORDERSERIESSHELVES_OPTIONS_DEFAULTS
plugin_prefs.defaults[STORE_LIBRARIES]                  = {}
plugin_prefs.defaults[UPDATE_OPTIONS_STORE_NAME]        = UPDATE_OPTIONS_DEFAULTS
plugin_prefs.defaults[BACKUP_OPTIONS_STORE_NAME]        = BACKUP_OPTIONS_DEFAULTS


try:
Example #18
0
from calibre.gui2.metadata.config import ConfigWidget as DefaultConfigWidget
from calibre.utils.config import JSONConfig

STORE_NAME = 'Options'
# KEY_MAX_PAGES = 'maxPages'
KEY_MAX_DOWNLOADS = 'maxDownloads'
KEY_GET_ADDITIONAL_INFO = 'getAdditionalInfo'
KEY_THRESHOLD = 'threshold'
KEY_TRY_EXCHANGING = 'tryExchanging'

DEFAULT_STORE_VALUES = {
    KEY_MAX_DOWNLOADS: 1,
}

# This is where all preferences for this plugin will be stored
plugin_prefs = JSONConfig('plugins/GoogleScholar')

# Set defaults
plugin_prefs.defaults[STORE_NAME] = DEFAULT_STORE_VALUES


def getOption(option_key):
    default_value = DEFAULT_STORE_VALUES[option_key]
    return plugin_prefs[STORE_NAME].get(option_key, default_value)


class ConfigWidget(DefaultConfigWidget):
    def __init__(self, plugin):
        DefaultConfigWidget.__init__(self, plugin)
        c = plugin_prefs[STORE_NAME]
Example #19
0
        library_config = old_prefs[library_id]
        set_library_config(library_config)
        del old_prefs[library_id]

    if library_config is None:
        #print("get prefs from db")
        library_config = db.prefs.get_namespaced(PREFS_NAMESPACE, PREFS_KEY_SETTINGS,
                                                 copy.deepcopy(default_prefs))
    return library_config

# This is where all preferences for this plugin *were* stored
# Remember that this name (i.e. plugins/epubsplit) is also
# in a global namespace, so make it as unique as possible.
# You should always prefix your config file name with plugins/,
# so as to ensure you dont accidentally clobber a calibre config file
old_prefs = JSONConfig('plugins/EpubSplit')

# fake out so I don't have to change the prefs calls anywhere.  The
# Java programmer in me is offended by op-overloading, but it's very
# tidy.
class PrefsFacade():
    def __init__(self,default_prefs):
        self.default_prefs = default_prefs
        self.libraryid = None
        self.current_prefs = None

    def _get_prefs(self):
        libraryid = get_library_uuid(get_gui().current_db)
        if self.current_prefs == None or self.libraryid != libraryid:
            #print("self.current_prefs == None(%s) or self.libraryid != libraryid(%s)"%(self.current_prefs == None,self.libraryid != libraryid))
            self.libraryid = libraryid
Example #20
0
class SpanDivEdit(Tool):
    name = 'SpanDivEdit'

    #: If True the user can choose to place this tool in the plugins toolbar
    allowed_in_toolbar = True

    #: If True the user can choose to place this tool in the plugins menu
    allowed_in_menu = True

    def create_action(self, for_toolbar=True):
        self.plugin_prefs = JSONConfig('plugins/{0}_SpanDivEdit'.format(PLUGIN_SAFE_NAME))
        self.plugin_prefs.defaults['parse_current'] = True

        # Create an action, this will be added to the plugins toolbar and
        # the plugins menu
        ac = QAction(get_icon('images/spandivedit_icon.png'), _('Edit Spans && Divs'), self.gui)
        self.restore_prefs()
        if not for_toolbar:
            # Register a keyboard shortcut for this toolbar action. We only
            # register it for the action created for the menu, not the toolbar,
            # to avoid a double trigger
            self.register_shortcut(ac, 'edit-spans-divs', default_keys=('Ctrl+Shift+Alt+E',))
        else:
            menu = QMenu()
            ac.setMenu(menu)
            checked_menu_item = menu.addAction(_('Edit current file only'), self.toggle_parse_current)
            checked_menu_item.setCheckable(True)
            checked_menu_item.setChecked(self.parse_current)
            menu.addSeparator()
            config_menu_item = menu.addAction(_('Customize'), self.show_configuration)
        ac.triggered.connect(self.dispatcher)
        return ac

    def toggle_parse_current(self):
        self.parse_current = not self.parse_current
        self.save_prefs()

    def dispatcher(self):
        container = self.current_container  # The book being edited as a container object
        if not container:
            return info_dialog(self.gui, _('No book open'),
                        _('Need to have a book open first.'), show=True)
        if self.parse_current:
            name = editor_name(self.gui.central.current_editor)
            if not name or container.mime_map[name] not in OEB_DOCS:
                return info_dialog(self.gui, _('Cannot Process'),
                        _('No file open for editing or the current file is not an (x)html file.'), show=True)

        self.cleanasawhistle = True
        self.changed_files = []

        from calibre_plugins.diaps_toolbag.dialogs import RemoveDialog
        dlg = RemoveDialog(self.gui)
        if dlg.exec_():
            criteria = dlg.getCriteria()

            # Ensure any in progress editing the user is doing is present in the container
            self.boss.commit_all_editors_to_container()
            self.boss.add_savepoint(_('Before: Span Div Edit'))

            try:
                self.process_files(criteria)
            except Exception:
                # Something bad happened report the error to the user
                import traceback
                error_dialog(self.gui, _('Failed'),
                    _('Failed to process divs or spans, click "Show details" for more info'),
                    det_msg=traceback.format_exc(), show=True)
                # Revert to the saved restore point
                self.boss.revert_requested(self.boss.global_undo.previous_container)
            else:
                if not self.cleanasawhistle:
                    # Show the user what changes we have made,
                    # allowing then to revert them if necessary
                    accepted = ResultsDialog(self.gui, self.changed_files).exec_()
                    if accepted == QDialog.Accepted:
                        self.boss.show_current_diff()
                    # Update the editor UI to take into account all the changes we
                    # have made
                    self.boss.apply_container_update_to_gui()
                else:
                    info_dialog(self.gui, _('Nothing changed'),
                    '<p>{0}'.format(_('Nothing matching your criteria was found.')), show=True)

    def process_files(self, criteria):
        container = self.current_container  # The book being edited as a container object

        if self.parse_current:
            name = editor_name(self.gui.central.current_editor)
            data = container.raw_data(name)
            htmlstr = self.delete_modify(data, criteria)
            if htmlstr != data:
                self.cleanasawhistle = False
                container.open(name, 'wb').write(htmlstr)
        else:
            from calibre_plugins.diaps_toolbag.dialogs import ShowProgressDialog
            d = ShowProgressDialog(self.gui, container, OEB_DOCS, criteria, self.delete_modify, _('Parsing'))
            self.cleanasawhistle = d.clean
            self.changed_files.extend(d.changed_files)

    def delete_modify(self, data, criteria):
        _parser = MarkupParser(data, srch_str=criteria[0], srch_method=criteria[1], tag=criteria[2], attrib=criteria[3],
                               action=criteria[4], new_tag=criteria[5], new_str=criteria[6], copy=criteria[7])

        htmlstr = _parser.processml()
        return htmlstr

    def show_configuration(self):
        from calibre_plugins.diaps_toolbag.span_div_config import ConfigWidget
        dlg = ConfigWidget(self.gui)
        if dlg.exec_():
            pass

    def restore_prefs(self):
        self.parse_current = self.plugin_prefs.get('parse_current')

    def save_prefs(self):
        self.plugin_prefs['parse_current'] = self.parse_current
Example #21
0
class CSScm2em(Tool):
    name = 'CSScm2em'

    #: If True the user can choose to place this tool in the plugins toolbar
    allowed_in_toolbar = True

    #: If True the user can choose to place this tool in the plugins menu
    allowed_in_menu = True

    def create_action(self, for_toolbar=True):
        self.plugin_prefs = JSONConfig('plugins/{0}_CSScm2em'.format(PLUGIN_SAFE_NAME))
        self.plugin_prefs.defaults['parse_current'] = True

        # Create an action, this will be added to the plugins toolbar and
        # the plugins menu
        ac = QAction(get_icon('images/css_icon.png'), _('Convert CSS cm to em'), self.gui)
        self.restore_prefs()
        if not for_toolbar:
            # Register a keyboard shortcut for this toolbar action. We only
            # register it for the action created for the menu, not the toolbar,
            # to avoid a double trigger
            self.register_shortcut(ac, 'css-cms-to-ems', default_keys=('Ctrl+Shift+Alt+C',))
        menu = QMenu()
        ac.setMenu(menu)
        checked_menu_item = menu.addAction(_('Convert current CSS file only'), self.toggle_parse_current)
        checked_menu_item.setCheckable(True)
        checked_menu_item.setChecked(self.parse_current)
        ac.triggered.connect(self.dispatcher)
        return ac

    def toggle_parse_current(self):
        self.parse_current = not self.parse_current
        self.save_prefs()

    def dispatcher(self):
        container = self.current_container
        if not container:
            return info_dialog(self.gui, _('No book open'),
                        _('Need to have a book open first.'), show=True)
        if self.parse_current:
            name = editor_name(self.gui.central.current_editor)
            if not name or container.mime_map[name] not in OEB_STYLES:
                return info_dialog(self.gui, _('Cannot Process'),
                        _('No file open for editing or the current file is not a CSS file.'), show=True)

        self.cleanasawhistle = True
        self.changed_files = []

        self.boss.commit_all_editors_to_container()
        self.boss.add_savepoint(_('Before: Convert CM to EM'))

        try:
            self.process_files()
        except Exception:
            # Something bad happened report the error to the user
            import traceback
            error_dialog(self.gui, _('Failed'),
                _('Failed to convert CMs to EMs, click "Show details" for more info'),
                det_msg=traceback.format_exc(), show=True)
            # Revert to the saved restore point
            self.boss.revert_requested(self.boss.global_undo.previous_container)
        else:
            if not self.cleanasawhistle:
                # Show the user what changes we have made,
                # allowing then to revert them if necessary
                accepted = ResultsDialog(self.gui, self.changed_files).exec_()
                if accepted == QDialog.Accepted:
                    self.boss.show_current_diff()
                # Update the editor UI to take into account all the changes we
                # have made
                self.boss.apply_container_update_to_gui()
            else:
                info_dialog(self.gui, _('Nothing Converted'),
                '<p>{0}'.format(_('No CM dimensions were found to convert.')), show=True)

    def process_files(self):
        def cssparse(sheet):
            SHEET_DIRTY = False
            for rule in sheet.cssRules:
                if rule.type in (rule.STYLE_RULE, rule.PAGE_RULE):
                    for property in rule.style:
                        val = property.propertyValue
                        for x in val:
                            if x.type == 'DIMENSION' and x.dimension == 'cm':
                                SHEET_DIRTY = True
                                x.cssText = self.convertcm2em(x.value) + 'em'
            if SHEET_DIRTY:
                self.cleanasawhistle = False
                self.changed_files.append(name)
                container.dirty(name)
        container = self.current_container  # The book being edited as a container object
        if self.parse_current:
            name = editor_name(self.gui.central.current_editor)
            sheet = container.parsed(name)
            cssparse(sheet)
        else:
            for name, mt in container.mime_map.iteritems():
                if mt in OEB_STYLES:
                    sheet = container.parsed(name)
                    cssparse(sheet)

    def restore_prefs(self):
        self.parse_current = self.plugin_prefs.get('parse_current')

    def save_prefs(self):
        self.plugin_prefs['parse_current'] = self.parse_current

    def convertcm2em(self, value):
        conv=2.37106301584
        return '%.2f' % (conv*float(value))
Example #22
0
from PyQt4.Qt import QWidget, QVBoxLayout, QLabel, QLineEdit

from calibre.utils.config import JSONConfig

# This is where all preferences for this plugin will be stored
# You should always prefix your config file name with plugins/,
# so as to ensure you dont accidentally clobber a calibre config file
prefs = JSONConfig('plugins/K4MobiDeDRM')

# Set defaults
prefs.defaults['pids'] = ""
prefs.defaults['serials'] = ""
prefs.defaults['WINEPREFIX'] = None


class ConfigWidget(QWidget):

    def __init__(self):
        QWidget.__init__(self)
        self.l = QVBoxLayout()
        self.setLayout(self.l)

        self.serialLabel = QLabel('eInk Kindle Serial numbers (First character B, 16 characters, use commas if more than one)')
        self.l.addWidget(self.serialLabel)

        self.serials = QLineEdit(self)
        self.serials.setText(prefs['serials'])
        self.l.addWidget(self.serials)
        self.serialLabel.setBuddy(self.serials)

        self.pidLabel = QLabel('Mobipocket PIDs (8 or 10 characters, use commas if more than one)')
Example #23
0
    QApplication, QDialog, QUrl, QFont, QFontDatabase, QLocale, QFontInfo)

ORG_NAME = 'KovidsBrain'
APP_UID  = 'libprs500'
from calibre import prints
from calibre.constants import (islinux, iswindows, isbsd, isfrozen, isosx,
        plugins, config_dir, filesystem_encoding, isxp)
from calibre.utils.config import Config, ConfigProxy, dynamic, JSONConfig
from calibre.ebooks.metadata import MetaInformation
from calibre.utils.date import UNDEFINED_DATE
from calibre.utils.localization import get_lang
from calibre.utils.filenames import expanduser
from calibre.utils.file_type_icons import EXT_MAP

# Setup gprefs {{{
gprefs = JSONConfig('gui')
defs = gprefs.defaults

native_menubar_defaults = {
    'action-layout-menubar': (
        'Add Books', 'Edit Metadata', 'Convert Books',
        'Choose Library', 'Save To Disk', 'Preferences',
        'Help',
        ),
    'action-layout-menubar-device': (
        'Add Books', 'Edit Metadata', 'Convert Books',
        'Location Manager', 'Send To Device',
        'Save To Disk', 'Preferences', 'Help',
        )
}
Example #24
0
def migrate_previous_viewer_prefs():
    new_prefs = vprefs
    if new_prefs['old_prefs_migrated']:
        return
    old_vprefs = JSONConfig('viewer')
    old_prefs = JSONConfig('viewer.py')
    with new_prefs:
        sd = new_prefs['session_data']
        fs = sd.get('standalone_font_settings', {})
        for k in ('serif', 'sans', 'mono'):
            defval = 'Liberation ' + k.capitalize()
            k += '_family'
            if old_prefs.get(k) and old_prefs[k] != defval:
                fs[k] = old_prefs[k]
        if old_prefs.get('standard_font') in ('serif', 'sans', 'mono'):
            fs['standard_font'] = old_prefs['standard_font']
        if old_prefs.get('minimum_font_size'
                         ) is not None and old_prefs['minimum_font_size'] != 8:
            fs['minimum_font_size'] = old_prefs['minimum_font_size']
        sd['standalone_font_settings'] = fs

        ms = sd.get('standalone_misc_settings', {})
        ms['remember_window_geometry'] = bool(
            old_prefs.get('remember_window_size', False))
        ms['remember_last_read'] = bool(
            old_prefs.get('remember_current_page', True))
        ms['save_annotations_in_ebook'] = bool(
            old_prefs.get('copy_bookmarks_to_file', True))
        ms['singleinstance'] = bool(old_vprefs.get('singleinstance', False))
        sd['standalone_misc_settings'] = ms

        for k in ('top', 'bottom'):
            v = old_prefs.get(k + '_margin')
            if v != 20 and v is not None:
                sd['margin_' + k] = v
        v = old_prefs.get('side_margin')
        if v is not None and v != 40:
            sd['margin_left'] = sd['margin_right'] = v // 2

        if old_prefs.get('user_css'):
            sd['user_stylesheet'] = old_prefs['user_css']

        cps = {'portrait': 0, 'landscape': 0}
        cp = old_prefs.get('cols_per_screen_portrait')
        if cp and cp > 1:
            cps['portrait'] = cp
        cl = old_prefs.get('cols_per_screen_landscape')
        if cl and cl > 1:
            cps['landscape'] = cp
        if cps['portrait'] or cps['landscape']:
            sd['columns_per_screen'] = cps
        if old_vprefs.get('in_paged_mode') is False:
            sd['read_mode'] = 'flow'

        new_prefs.set('session_data', sd)
        new_prefs.set('old_prefs_migrated', True)
Example #25
0
class FontScanner(Thread):

    CACHE_VERSION = 2

    def __init__(self, folders=[], allowed_extensions={'ttf', 'otf'}):
        Thread.__init__(self)
        self.folders = folders + font_dirs() + [
            os.path.join(config_dir, 'fonts'),
            P('fonts/liberation')
        ]
        self.folders = [
            os.path.normcase(os.path.abspath(f)) for f in self.folders
        ]
        self.font_families = ()
        self.allowed_extensions = allowed_extensions

    # API {{{
    def find_font_families(self):
        self.join()
        return self.font_families

    def fonts_for_family(self, family):
        '''
        Return a list of the faces belonging to the specified family. The first
        face is the "Regular" face of family. Each face is a dictionary with
        many keys, the most important of which are: path, font-family,
        font-weight, font-style, font-stretch. The font-* properties follow the
        CSS 3 Fonts specification.
        '''
        self.join()
        try:
            return self.font_family_map[icu_lower(family)]
        except KeyError:
            raise NoFonts('No fonts found for the family: %r' % family)

    def legacy_fonts_for_family(self, family):
        '''
        Return a simple set of regular, bold, italic and bold-italic faces for
        the specified family. Returns a dictionary with each element being a
        2-tuple of (path to font, full font name) and the keys being: normal,
        bold, italic, bi.
        '''
        ans = {}
        try:
            faces = self.fonts_for_family(family)
        except NoFonts:
            return ans
        for i, face in enumerate(faces):
            if i == 0:
                key = 'normal'
            elif face['font-style'] in {'italic', 'oblique'}:
                key = 'bi' if face['font-weight'] == 'bold' else 'italic'
            elif face['font-weight'] == 'bold':
                key = 'bold'
            else:
                continue
            ans[key] = (face['path'], face['full_name'])
        return ans

    def get_font_data(self, font_or_path):
        path = font_or_path
        if isinstance(font_or_path, dict):
            path = font_or_path['path']
        with lopen(path, 'rb') as f:
            return f.read()

    def find_font_for_text(self,
                           text,
                           allowed_families={'serif', 'sans-serif'},
                           preferred_families=('serif', 'sans-serif',
                                               'monospace', 'cursive',
                                               'fantasy')):
        '''
        Find a font on the system capable of rendering the given text.

        Returns a font family (as given by fonts_for_family()) that has a
        "normal" font and that can render the supplied text. If no such font
        exists, returns None.

        :return: (family name, faces) or None, None
        '''
        from calibre.utils.fonts.utils import (supports_text,
                                               panose_to_css_generic_family,
                                               get_printable_characters)
        if not isinstance(text, unicode_type):
            raise TypeError(u'%r is not unicode' % text)
        text = get_printable_characters(text)
        found = {}

        def filter_faces(font):
            try:
                raw = self.get_font_data(font)
                return supports_text(raw, text)
            except:
                pass
            return False

        for family in self.find_font_families():
            faces = list(filter(filter_faces, self.fonts_for_family(family)))
            if not faces:
                continue
            generic_family = panose_to_css_generic_family(faces[0]['panose'])
            if generic_family in allowed_families or generic_family == preferred_families[
                    0]:
                return (family, faces)
            elif generic_family not in found:
                found[generic_family] = (family, faces)

        for f in preferred_families:
            if f in found:
                return found[f]
        return None, None

    # }}}

    def reload_cache(self):
        if not hasattr(self, 'cache'):
            from calibre.utils.config import JSONConfig
            self.cache = JSONConfig('fonts/scanner_cache')
        else:
            self.cache.refresh()
        if self.cache.get('version', None) != self.CACHE_VERSION:
            self.cache.clear()
        self.cached_fonts = self.cache.get('fonts', {})

    def run(self):
        self.do_scan()

    def do_scan(self):
        self.reload_cache()

        if isworker:
            # Dont scan font files in worker processes, use whatever is
            # cached. Font files typically dont change frequently enough to
            # justify a rescan in a worker process.
            self.build_families()
            return

        cached_fonts = self.cached_fonts.copy()
        self.cached_fonts.clear()
        for folder in self.folders:
            if not os.path.isdir(folder):
                continue
            try:
                files = tuple(walk(folder))
            except EnvironmentError as e:
                if DEBUG:
                    prints('Failed to walk font folder:', folder,
                           as_unicode(e))
                continue
            for candidate in files:
                if (candidate.rpartition('.')[-1].lower()
                        not in self.allowed_extensions
                        or not os.path.isfile(candidate)):
                    continue
                candidate = os.path.normcase(os.path.abspath(candidate))
                try:
                    s = os.stat(candidate)
                except EnvironmentError:
                    continue
                fileid = '{0}||{1}:{2}'.format(candidate, s.st_size,
                                               s.st_mtime)
                if fileid in cached_fonts:
                    # Use previously cached metadata, since the file size and
                    # last modified timestamp have not changed.
                    self.cached_fonts[fileid] = cached_fonts[fileid]
                    continue
                try:
                    self.read_font_metadata(candidate, fileid)
                except Exception as e:
                    if DEBUG:
                        prints('Failed to read metadata from font file:',
                               candidate, as_unicode(e))
                    continue

        if frozenset(cached_fonts) != frozenset(self.cached_fonts):
            # Write out the cache only if some font files have changed
            self.write_cache()

        self.build_families()

    def build_families(self):
        self.font_family_map, self.font_families = build_families(
            self.cached_fonts, self.folders)

    def write_cache(self):
        with self.cache:
            self.cache['version'] = self.CACHE_VERSION
            self.cache['fonts'] = self.cached_fonts

    def force_rescan(self):
        self.cached_fonts = {}
        self.write_cache()

    def read_font_metadata(self, path, fileid):
        with lopen(path, 'rb') as f:
            try:
                fm = FontMetadata(f)
            except UnsupportedFont:
                self.cached_fonts[fileid] = {}
            else:
                data = fm.to_dict()
                data['path'] = path
                self.cached_fonts[fileid] = data

    def dump_fonts(self):
        self.join()
        for family in self.font_families:
            prints(family)
            for font in self.fonts_for_family(family):
                prints('\t%s: %s' % (font['full_name'], font['path']))
                prints(end='\t')
                for key in ('font-stretch', 'font-weight', 'font-style'):
                    prints('%s: %s' % (key, font[key]), end=' ')
                prints()
                prints(
                    '\tSub-family:', font['wws_subfamily_name']
                    or font['preferred_subfamily_name']
                    or font['subfamily_name'])
                prints()
            prints()
#!/usr/bin/env python2
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import,
                        print_function)

__license__   = 'GPL v3'
__copyright__ = '2016, Samreen Zarroug & Alex Mayer'
__docformat__ = 'restructuredtext en'

from PyQt5.Qt import QWidget, QVBoxLayout, QHBoxLayout, QCheckBox, QGroupBox

from calibre.utils.config import JSONConfig

prefs = JSONConfig('plugins/xray_creator')

# Set defaults
prefs.defaults['spoilers'] = False
prefs.defaults['send_to_device'] = True
prefs.defaults['create_xray_when_sending'] = True
prefs.defaults['mobi'] = True
prefs.defaults['azw3'] = True

class ConfigWidget(QWidget):
    def __init__(self):
        QWidget.__init__(self)
        self.l = QVBoxLayout()
        self.setLayout(self.l)

        self.spoilers = QCheckBox('Use spoilers when creating x-ray')
        self.spoilers.setChecked(prefs['spoilers'])
        self.l.addWidget(self.spoilers)
Example #27
0
#!/usr/bin/env python
# vim:fileencoding=utf-8
from __future__ import (unicode_literals, division, absolute_import,
                        print_function)

__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'

from calibre.utils.config import JSONConfig
tprefs = JSONConfig('tweak_book_gui')

tprefs.defaults['editor_theme'] = None
tprefs.defaults['editor_font_family'] = None
tprefs.defaults['editor_font_size'] = 12
tprefs.defaults['editor_line_wrap'] = True
tprefs.defaults['preview_refresh_time'] = 2

_current_container = None

def current_container():
    return _current_container

def set_current_container(container):
    global _current_container
    _current_container = container

def elided_text(font, text, width=200, mode=None):
    from PyQt4.Qt import QFontMetrics, Qt
    if mode is None:
        mode = Qt.ElideMiddle
    fm = QFontMetrics(font)
class MobileReadStore(BasicStoreConfig, StorePlugin):
    def __init__(self, *args, **kwargs):
        StorePlugin.__init__(self, *args, **kwargs)
        self.lock = Lock()

    @property
    def cache(self):
        if not hasattr(self, '_mr_cache'):
            from calibre.utils.config import JSONConfig
            self._mr_cache = JSONConfig('mobileread_get_books')
            self._mr_cache.file_path = os.path.join(
                cache_dir(), 'mobileread_get_books.json')
            self._mr_cache.refresh()
        return self._mr_cache

    def open(self, parent=None, detail_item=None, external=False):
        url = 'https://www.mobileread.com/'

        if external or self.config.get('open_external', False):
            open_url(QUrl(detail_item if detail_item else url))
        else:
            if detail_item:
                d = WebStoreDialog(self.gui, url, parent, detail_item)
                d.setWindowTitle(self.name)
                d.set_tags(self.config.get('tags', ''))
                d.exec_()
            else:
                self.update_cache(parent, 30)
                d = MobileReadStoreDialog(self, parent)
                d.setWindowTitle(self.name)
                d.exec_()

    def search(self, query, max_results=10, timeout=60):
        books = self.get_book_list()

        if not books:
            return

        sf = SearchFilter(books)
        matches = sf.parse(query.decode('utf-8', 'replace'))

        for book in matches:
            book.price = '$0.00'
            book.drm = SearchResult.DRM_UNLOCKED
            yield book

    def update_cache(self,
                     parent=None,
                     timeout=10,
                     force=False,
                     suppress_progress=False):
        if self.lock.acquire(False):
            try:
                update_thread = CacheUpdateThread(self.cache,
                                                  self.seralize_books, timeout)
                if not suppress_progress:
                    progress = CacheProgressDialog(parent)
                    progress.set_message(
                        _('Updating MobileRead book cache...'))

                    update_thread.total_changed.connect(progress.set_total)
                    update_thread.update_progress.connect(
                        progress.set_progress)
                    update_thread.update_details.connect(progress.set_details)
                    progress.rejected.connect(update_thread.abort)

                    progress.open()
                    update_thread.start()
                    while update_thread.is_alive() and not progress.canceled:
                        QCoreApplication.processEvents()

                    if progress.isVisible():
                        progress.accept()
                    return not progress.canceled
                else:
                    update_thread.start()
            finally:
                self.lock.release()

    def get_book_list(self):
        return self.deseralize_books(self.cache.get('book_list', []))

    def seralize_books(self, books):
        sbooks = []
        for b in books:
            data = {}
            data['author'] = b.author
            data['title'] = b.title
            data['detail_item'] = b.detail_item
            data['formats'] = b.formats
            sbooks.append(data)
        return sbooks

    def deseralize_books(self, sbooks):
        books = []
        for s in sbooks:
            b = SearchResult()
            b.author = s.get('author', '')
            b.title = s.get('title', '')
            b.detail_item = s.get('detail_item', '')
            b.formats = s.get('formats', '')
            books.append(b)
        return books
Example #29
0
    QImage, Qt, QFont, QPainter, QPointF, QTextLayout, QTextOption,
    QFontMetrics, QTextCharFormat, QColor, QRect, QBrush, QLinearGradient,
    QPainterPath, QPen, QRectF, QTransform, QRadialGradient
)

from calibre import force_unicode, fit_image
from calibre.constants import __appname__, __version__
from calibre.ebooks.metadata import fmt_sidx
from calibre.ebooks.metadata.book.base import Metadata
from calibre.ebooks.metadata.book.formatter import SafeFormat
from calibre.gui2 import ensure_app, config, load_builtin_fonts, pixmap_to_data
from calibre.utils.cleantext import clean_ascii_chars, clean_xml_chars
from calibre.utils.config import JSONConfig

# Default settings {{{
cprefs = JSONConfig('cover_generation')
cprefs.defaults['title_font_size'] = 120  # px
cprefs.defaults['subtitle_font_size'] = 80  # px
cprefs.defaults['footer_font_size'] = 80  # px
cprefs.defaults['cover_width'] = 1200  # px
cprefs.defaults['cover_height'] = 1600  # px
cprefs.defaults['title_font_family'] = None
cprefs.defaults['subtitle_font_family'] = None
cprefs.defaults['footer_font_family'] = None
cprefs.defaults['color_themes'] = {}
cprefs.defaults['disabled_color_themes'] = []
cprefs.defaults['disabled_styles'] = []
cprefs.defaults['title_template'] = '<b>{title}'
cprefs.defaults['subtitle_template'] = '''{series:'test($, strcat("<i>", $, "</i> - ", raw_field("formatted_series_index")), "")'}'''
cprefs.defaults['footer_template'] = r'''program:
# Show at most two authors, on separate lines.
Example #30
0
                        print_function)

__license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <*****@*****.**>'
__docformat__ = 'restructuredtext en'

from PyQt5.Qt import QWidget, QHBoxLayout, QLabel, QLineEdit

from calibre.utils.config import JSONConfig

# This is where all preferences for this plugin will be stored
# Remember that this name (i.e. plugins/interface_demo) is also
# in a global namespace, so make it as unique as possible.
# You should always prefix your config file name with plugins/,
# so as to ensure you dont accidentally clobber a calibre config file
prefs = JSONConfig('plugins/interface_demo')

# Set defaults
prefs.defaults['hello_world_msg'] = 'Hello, World!'


class ConfigWidget(QWidget):
    def __init__(self):
        QWidget.__init__(self)
        self.l = QHBoxLayout()
        self.setLayout(self.l)

        self.label = QLabel('Hello world &message:')
        self.l.addWidget(self.label)

        self.msg = QLineEdit(self)
Example #31
0
__license__   = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <*****@*****.**>'
__docformat__ = 'restructuredtext en'

import re, threading
from future_builtins import map

from calibre import browser, random_user_agent
from calibre.customize import Plugin
from calibre.utils.config import JSONConfig
from calibre.utils.titlecase import titlecase
from calibre.utils.icu import capitalize, lower, upper
from calibre.ebooks.metadata import check_isbn

msprefs = JSONConfig('metadata_sources/global.json')
msprefs.defaults['txt_comments'] = False
msprefs.defaults['ignore_fields'] = []
msprefs.defaults['user_default_ignore_fields'] = []
msprefs.defaults['max_tags'] = 20
msprefs.defaults['wait_after_first_identify_result'] = 30 # seconds
msprefs.defaults['wait_after_first_cover_result'] = 60 # seconds
msprefs.defaults['swap_author_names'] = False
msprefs.defaults['fewer_tags'] = True
msprefs.defaults['find_first_edition_date'] = False

# Google covers are often poor quality (scans/errors) but they have high
# resolution, so they trump covers from better sources. So make sure they
# are only used if no other covers are found.
msprefs.defaults['cover_priorities'] = {'Google':2, 'Google Images':2, 'Big Book Search':2}
Example #32
0
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>

import os
from calibre.constants import config_dir
from calibre.utils.config import JSONConfig

vprefs = JSONConfig('viewer-webengine')
viewer_config_dir = os.path.join(config_dir, 'viewer')
vprefs.defaults['session_data'] = {}
vprefs.defaults['local_storage'] = {}
vprefs.defaults['main_window_state'] = None
vprefs.defaults['main_window_geometry'] = None
vprefs.defaults['old_prefs_migrated'] = False
vprefs.defaults['bookmarks_sort'] = 'title'
vprefs.defaults['highlight_export_format'] = 'txt'
vprefs.defaults['auto_update_lookup'] = True


def get_session_pref(name, default=None, group='standalone_misc_settings'):
    sd = vprefs['session_data']
    g = sd.get(group, {}) if group else sd
    return g.get(name, default)
Example #33
0
                        print_function)

__license__   = 'GPL v3'
__copyright__ = '2014, Kenny Billiau <*****@*****.**>'
__docformat__ = 'restructuredtext en'

from PyQt4.Qt import QWidget, QHBoxLayout, QLabel, QLineEdit

from calibre.utils.config import JSONConfig

# This is where all preferences for this plugin will be stored
# Remember that this name (i.e. plugins/interface_demo) is also
# in a global namespace, so make it as unique as possible.
# You should always prefix your config file name with plugins/,
# so as to ensure you dont accidentally clobber a calibre config file
prefs = JSONConfig('plugins/opml')

# Set defaults
prefs.defaults['oldest_article'] = '7'
prefs.defaults['max_articles'] = '100'

class ConfigWidget(QWidget):

    def __init__(self):
        QWidget.__init__(self)
        self.l = QHBoxLayout()
        self.setLayout(self.l)

        self.oldest_articlel = QLabel('Oldest article:')
        self.l.addWidget(self.oldest_articlel)
Example #34
0
from calibre import prints
from calibre.constants import plugins, config_dir
from calibre.spell import parse_lang_code
from calibre.utils.config import JSONConfig
from calibre.utils.icu import capitalize
from calibre.utils.localization import get_lang, get_system_locale

Dictionary = namedtuple(
    'Dictionary', 'primary_locale locales dicpath affpath builtin name id')
LoadedDictionary = namedtuple('Dictionary',
                              'primary_locale locales obj builtin name id')
hunspell = plugins['hunspell'][0]
if hunspell is None:
    raise RuntimeError('Failed to load hunspell: %s' % plugins['hunspell'][1])
dprefs = JSONConfig('dictionaries/prefs.json')
dprefs.defaults['preferred_dictionaries'] = {}
dprefs.defaults['preferred_locales'] = {}
dprefs.defaults['user_dictionaries'] = [{
    'name': _('Default'),
    'is_active': True,
    'words': []
}]
not_present = object()


class UserDictionary(object):

    __slots__ = ('name', 'is_active', 'words')

    def __init__(self, **kwargs):
Example #35
0
import textwrap

from PyQt5.Qt import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,
                      QListWidget, QIcon, QSize, QComboBox, QLineEdit,
                      QListWidgetItem, QStyledItemDelegate, QStaticText, Qt,
                      QStyle, QToolButton, QInputDialog, QMenu, pyqtSignal)

from calibre.ebooks.metadata.tag_mapper import map_tags, compile_pat
from calibre.gui2 import error_dialog, Application, question_dialog
from calibre.gui2.ui import get_gui
from calibre.gui2.widgets2 import Dialog
from calibre.utils.config import JSONConfig
from calibre.utils.localization import localize_user_manual_link
from polyglot.builtins import iteritems, unicode_type, range, filter

tag_maps = JSONConfig('tag-map-rules')


def intelligent_strip(action, val):
    ans = val.strip()
    if not ans and action == 'split':
        ans = ' '
    return ans


class QueryEdit(QLineEdit):
    def contextMenuEvent(self, ev):
        menu = self.createStandardContextMenu()
        self.parent().specialise_context_menu(menu)
        menu.exec_(ev.globalPos())
Example #36
0
            icon = QIcon(I('blank.png'))
        else:
            icon = QIcon(pmap)
    return icon, entry.get('name', entry.get('Name')) or _('Unknown')


if iswindows:
    # Windows {{{
    import subprocess

    from calibre.utils.open_with.windows import (load_icon_for_cmdline,
                                                 load_icon_resource)
    from calibre.utils.winreg.default_programs import (find_programs,
                                                       friendly_app_name)
    from calibre_extensions import winutil
    oprefs = JSONConfig('windows_open_with')

    def entry_sort_key(entry):
        return sort_key(entry.get('name') or '')

    def icon_for_entry(entry, delete_icon_resource=False, as_data=False):
        res = entry.pop(
            'icon_resource',
            None) if delete_icon_resource else entry.get('icon_resource')
        if res is None:
            return load_icon_for_cmdline(entry['cmdline'], as_data=as_data)
        try:
            return load_icon_resource(res, as_data=as_data)
        except Exception:
            import traceback
            traceback.print_exc()
Example #37
0
#!/usr/bin/env python
# vim:fileencoding=utf-8


__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'

import string
from polyglot.builtins import iteritems, map

from calibre.utils.config import JSONConfig
from calibre.spell.dictionary import Dictionaries, parse_lang_code

CONTAINER_DND_MIMETYPE = 'application/x-calibre-container-name-list'
tprefs = JSONConfig('tweak_book_gui')
d = tprefs.defaults

d['editor_theme'] = None
d['editor_font_family'] = None
d['editor_font_size'] = 12
d['editor_line_wrap'] = True
d['editor_tab_stop_width'] = 2
d['editor_show_char_under_cursor'] = True
d['replace_entities_as_typed'] = True
d['preview_refresh_time'] = 2
d['choose_tweak_fmt'] = True
d['tweak_fmt_order'] = ['EPUB', 'AZW3']
d['update_metadata_from_calibre'] = True
d['nestable_dock_widgets'] = False
d['dock_top_left'] = 'horizontal'
d['dock_top_right'] = 'horizontal'
Example #38
0
        if len(toc) == 0:
            return error_dialog(
                self,
                _('No items found'),
                _('No files were found that could be added to the Table of Contents.'
                  ),
                show=True)
        self.insert_toc_fragment(toc)

    def undo(self):
        self.tocw.pop_history()


# }}}

te_prefs = JSONConfig('toc-editor')


class TOCEditor(QDialog):  # {{{

    explode_done = pyqtSignal(object)
    writing_done = pyqtSignal(object)

    def __init__(self, pathtobook, title=None, parent=None, prefs=None):
        QDialog.__init__(self, parent)
        self.prefs = prefs or te_prefs
        self.pathtobook = pathtobook
        self.working = True

        t = title or os.path.basename(pathtobook)
        self.book_title = t
Example #39
0
#!/usr/bin/env  python2
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
'''
Builtin recipes.
'''
import re, time, io
from calibre.web.feeds.news import (BasicNewsRecipe, CustomIndexRecipe,
                                    AutomaticNewsRecipe, CalibrePeriodical)
from calibre.ebooks.BeautifulSoup import BeautifulSoup
from calibre.utils.config import JSONConfig

basic_recipes = (BasicNewsRecipe, AutomaticNewsRecipe, CustomIndexRecipe,
                 CalibrePeriodical)

custom_recipes = JSONConfig('custom_recipes/index.json')


def custom_recipe_filename(id_, title):
    from calibre.utils.filenames import ascii_filename
    return ascii_filename(title[:50]) + \
                        ('_%s.recipe'%id_)


def compile_recipe(src):
    '''
    Compile the code in src and return a recipe object, if found.

    :param src: Python source code as bytestring or unicode object

    :return: Recipe class or None, if no such class was found in src
Example #40
0
from calibre.gui2 import choose_images, error_dialog, safe_open_url
from calibre.gui2.webengine import (Bridge, RestartingWebEngineView,
                                    create_script, from_js, insert_scripts,
                                    secure_webengine, to_js)
from calibre.srv.code import get_translations_data
from calibre.utils.config import JSONConfig
from calibre.utils.serialize import json_loads
from polyglot.builtins import as_bytes, iteritems

try:
    from PyQt5 import sip
except ImportError:
    import sip

SANDBOX_HOST = FAKE_HOST.rpartition('.')[0] + '.sandbox'
vprefs = JSONConfig('viewer-webengine')
viewer_config_dir = os.path.join(config_dir, 'viewer')
vprefs.defaults['session_data'] = {}
vprefs.defaults['local_storage'] = {}
vprefs.defaults['main_window_state'] = None
vprefs.defaults['main_window_geometry'] = None
vprefs.defaults['old_prefs_migrated'] = False

# Override network access to load data from the book {{{


def set_book_path(path, pathtoebook):
    set_book_path.pathtoebook = pathtoebook
    set_book_path.path = os.path.abspath(path)
    set_book_path.metadata = get_data('calibre-book-metadata.json')[0]
    set_book_path.manifest, set_book_path.manifest_mime = get_data(
Example #41
0
from calibre.gui2.viewer.toc import TOC
from calibre.gui2.widgets import ProgressIndicator
from calibre.gui2 import (Application, ORG_NAME, APP_UID, choose_files,
                          info_dialog, error_dialog, open_url,
                          setup_gui_option_parser)
from calibre.ebooks.oeb.iterator.book import EbookIterator
from calibre.ebooks import DRMError
from calibre.constants import islinux, filesystem_encoding
from calibre.utils.config import Config, StringConfig, JSONConfig
from calibre.customize.ui import available_input_formats
from calibre import as_unicode, force_unicode, isbytestring
from calibre.ptempfile import reset_base_dir
from calibre.utils.zipfile import BadZipfile
from calibre.utils.localization import canonicalize_lang, lang_as_iso639_1, get_lang

vprefs = JSONConfig('viewer')
dprefs = JSONConfig('viewer_dictionaries')
dprefs.defaults['word_lookups'] = {}


class Worker(Thread):
    def run(self):
        try:
            Thread.run(self)
            self.exception = self.traceback = None
        except BadZipfile:
            self.exception = _(
                'This ebook is corrupted and cannot be opened. If you '
                'downloaded it from somewhere, try downloading it again.')
            self.traceback = ''
        except Exception as err:
Example #42
0
class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):

    s_r_functions = {''              : lambda x: x,
                            _('Lower Case') : lambda x: icu_lower(x),
                            _('Upper Case') : lambda x: icu_upper(x),
                            _('Title Case') : lambda x: titlecase(x),
                            _('Capitalize') : lambda x: capitalize(x),
                    }

    s_r_match_modes = [_('Character match'),
                            _('Regular Expression'),
                      ]

    s_r_replace_modes = [_('Replace field'),
                            _('Prepend to field'),
                            _('Append to field'),
                        ]

    def __init__(self, window, rows, model, tab, refresh_books):
        ResizableDialog.__init__(self, window)
        Ui_MetadataBulkDialog.__init__(self)
        self.model = model
        self.db = model.db
        self.refresh_book_list.setChecked(gprefs['refresh_book_list_on_bulk_edit'])
        self.refresh_book_list.toggled.connect(self.save_refresh_booklist)
        self.ids = [self.db.id(r) for r in rows]
        self.first_title = self.db.title(self.ids[0], index_is_id=True)
        self.cover_clone.setToolTip(unicode(self.cover_clone.toolTip()) + ' (%s)' % self.first_title)
        self.box_title.setText('<p>' +
                _('Editing meta information for <b>%d books</b>') %
                len(rows))
        self.write_series = False
        self.changed = False
        self.refresh_books = refresh_books
        self.comments = null
        self.comments_button.clicked.connect(self.set_comments)

        all_tags = self.db.all_tags()
        self.tags.update_items_cache(all_tags)
        self.remove_tags.update_items_cache(all_tags)

        self.initialize_combos()

        for f in sorted(self.db.all_formats()):
            self.remove_format.addItem(f)

        self.remove_format.setCurrentIndex(-1)

        self.series.currentIndexChanged[int].connect(self.series_changed)
        self.series.editTextChanged.connect(self.series_changed)
        self.tag_editor_button.clicked.connect(self.tag_editor)
        self.autonumber_series.stateChanged[int].connect(self.auto_number_changed)
        self.pubdate.setMinimumDateTime(UNDEFINED_QDATETIME)
        self.pubdate_cw = CalendarWidget(self.pubdate)
        self.pubdate.setCalendarWidget(self.pubdate_cw)
        pubdate_format = tweaks['gui_pubdate_display_format']
        if pubdate_format is not None:
            self.pubdate.setDisplayFormat(pubdate_format)
        self.pubdate.setSpecialValueText(_('Undefined'))
        self.clear_pubdate_button.clicked.connect(self.clear_pubdate)
        self.pubdate.dateTimeChanged.connect(self.do_apply_pubdate)
        self.adddate.setDateTime(QDateTime.currentDateTime())
        self.adddate.setMinimumDateTime(UNDEFINED_QDATETIME)
        adddate_format = tweaks['gui_timestamp_display_format']
        if adddate_format is not None:
            self.adddate.setDisplayFormat(adddate_format)
        self.adddate.setSpecialValueText(_('Undefined'))
        self.clear_adddate_button.clicked.connect(self.clear_adddate)
        self.adddate.dateTimeChanged.connect(self.do_apply_adddate)

        if len(self.db.custom_field_keys(include_composites=False)) == 0:
            self.central_widget.removeTab(1)
        else:
            self.create_custom_column_editors()

        self.prepare_search_and_replace()

        self.button_box.clicked.connect(self.button_clicked)
        self.button_box.button(QDialogButtonBox.Apply).setToolTip(_(
            'Immediately make all changes without closing the dialog. '
            'This operation cannot be canceled or undone'))
        self.do_again = False
        self.central_widget.setCurrentIndex(tab)
        geom = gprefs.get('bulk_metadata_window_geometry', None)
        if geom is not None:
            self.restoreGeometry(bytes(geom))
        ct = gprefs.get('bulk_metadata_window_tab', 0)
        self.central_widget.setCurrentIndex(ct)
        self.languages.init_langs(self.db)
        self.languages.setEditText('')
        self.authors.setFocus(Qt.OtherFocusReason)
        self.exec_()

    def set_comments(self):
        from calibre.gui2.dialogs.comments_dialog import CommentsDialog
        d = CommentsDialog(self, '' if self.comments is null else (self.comments or ''), _('Comments'))
        if d.exec_() == d.Accepted:
            self.comments = d.textbox.html
            b = self.comments_button
            b.setStyleSheet('QPushButton { font-weight: bold }')
            if unicode(b.text())[-1] != '*':
                b.setText(unicode(b.text()) + ' *')

    def save_refresh_booklist(self, *args):
        gprefs['refresh_book_list_on_bulk_edit'] = bool(self.refresh_book_list.isChecked())

    def save_state(self, *args):
        gprefs['bulk_metadata_window_geometry'] = \
            bytearray(self.saveGeometry())
        gprefs['bulk_metadata_window_tab'] = self.central_widget.currentIndex()

    def do_apply_pubdate(self, *args):
        self.apply_pubdate.setChecked(True)

    def clear_pubdate(self, *args):
        self.pubdate.setDateTime(UNDEFINED_QDATETIME)

    def do_apply_adddate(self, *args):
        self.apply_adddate.setChecked(True)

    def clear_adddate(self, *args):
        self.adddate.setDateTime(UNDEFINED_QDATETIME)

    def button_clicked(self, which):
        if which == self.button_box.button(QDialogButtonBox.Apply):
            self.do_again = True
            self.accept()

    # S&R {{{
    def prepare_search_and_replace(self):
        self.search_for.initialize('bulk_edit_search_for')
        self.replace_with.initialize('bulk_edit_replace_with')
        self.s_r_template.initialize('bulk_edit_template')
        self.test_text.initialize('bulk_edit_test_test')
        self.all_fields = ['']
        self.writable_fields = ['']
        fm = self.db.field_metadata
        for f in fm:
            if (f in ['author_sort'] or
                    (fm[f]['datatype'] in ['text', 'series', 'enumeration', 'comments']
                     and fm[f].get('search_terms', None)
                     and f not in ['formats', 'ondevice', 'series_sort']) or
                    (fm[f]['datatype'] in ['int', 'float', 'bool', 'datetime'] and
                     f not in ['id', 'timestamp'])):
                self.all_fields.append(f)
                self.writable_fields.append(f)
            if fm[f]['datatype'] == 'composite':
                self.all_fields.append(f)
        self.all_fields.sort()
        self.all_fields.insert(1, '{template}')
        self.writable_fields.sort()
        self.search_field.setMaxVisibleItems(25)
        self.destination_field.setMaxVisibleItems(25)
        self.testgrid.setColumnStretch(1, 1)
        self.testgrid.setColumnStretch(2, 1)
        offset = 10
        self.s_r_number_of_books = min(10, len(self.ids))
        for i in range(1,self.s_r_number_of_books+1):
            w = QLabel(self.tabWidgetPage3)
            w.setText(_('Book %d:')%i)
            self.testgrid.addWidget(w, i+offset, 0, 1, 1)
            w = QLineEdit(self.tabWidgetPage3)
            w.setReadOnly(True)
            name = 'book_%d_text'%i
            setattr(self, name, w)
            self.book_1_text.setObjectName(name)
            self.testgrid.addWidget(w, i+offset, 1, 1, 1)
            w = QLineEdit(self.tabWidgetPage3)
            w.setReadOnly(True)
            name = 'book_%d_result'%i
            setattr(self, name, w)
            self.book_1_text.setObjectName(name)
            self.testgrid.addWidget(w, i+offset, 2, 1, 1)

        ident_types = sorted(self.db.get_all_identifier_types(), key=sort_key)
        self.s_r_dst_ident.setCompleter(QCompleter(ident_types))
        try:
            self.s_r_dst_ident.setPlaceholderText(_('Enter an identifier type'))
        except:
            pass
        self.s_r_src_ident.addItems(ident_types)

        self.main_heading = _(
                 '<b>You can destroy your library using this feature.</b> '
                 'Changes are permanent. There is no undo function. '
                 'You are strongly encouraged to back up your library '
                 'before proceeding.<p>'
                 'Search and replace in text fields using character matching '
                 'or regular expressions. ')

        self.character_heading = _(
                 'In character mode, the field is searched for the entered '
                 'search text. The text is replaced by the specified replacement '
                 'text everywhere it is found in the specified field. After '
                 'replacement is finished, the text can be changed to '
                 'upper-case, lower-case, or title-case. If the case-sensitive '
                 'check box is checked, the search text must match exactly. If '
                 'it is unchecked, the search text will match both upper- and '
                 'lower-case letters'
                 )

        self.regexp_heading = _(
                 'In regular expression mode, the search text is an '
                 'arbitrary python-compatible regular expression. The '
                 'replacement text can contain backreferences to parenthesized '
                 'expressions in the pattern. The search is not anchored, '
                 'and can match and replace multiple times on the same string. '
                 'The modification functions (lower-case etc) are applied to the '
                 'matched text, not to the field as a whole. '
                 'The destination box specifies the field where the result after '
                 'matching and replacement is to be assigned. You can replace '
                 'the text in the field, or prepend or append the matched text. '
                 'See <a href="http://docs.python.org/library/re.html"> '
                 'this reference</a> for more information on python\'s regular '
                 'expressions, and in particular the \'sub\' function.'
                 )

        self.search_mode.addItems(self.s_r_match_modes)
        self.search_mode.setCurrentIndex(dynamic.get('s_r_search_mode', 0))
        self.replace_mode.addItems(self.s_r_replace_modes)
        self.replace_mode.setCurrentIndex(0)

        self.s_r_search_mode = 0
        self.s_r_error = None
        self.s_r_obj = None

        self.replace_func.addItems(sorted(self.s_r_functions.keys()))
        self.search_mode.currentIndexChanged[int].connect(self.s_r_search_mode_changed)
        self.search_field.currentIndexChanged[int].connect(self.s_r_search_field_changed)
        self.destination_field.currentIndexChanged[int].connect(self.s_r_destination_field_changed)

        self.replace_mode.currentIndexChanged[int].connect(self.s_r_paint_results)
        self.replace_func.currentIndexChanged[str].connect(self.s_r_paint_results)
        self.search_for.editTextChanged[str].connect(self.s_r_paint_results)
        self.replace_with.editTextChanged[str].connect(self.s_r_paint_results)
        self.test_text.editTextChanged[str].connect(self.s_r_paint_results)
        self.comma_separated.stateChanged.connect(self.s_r_paint_results)
        self.case_sensitive.stateChanged.connect(self.s_r_paint_results)
        self.s_r_src_ident.currentIndexChanged[int].connect(self.s_r_identifier_type_changed)
        self.s_r_dst_ident.textChanged.connect(self.s_r_paint_results)
        self.s_r_template.lost_focus.connect(self.s_r_template_changed)
        self.central_widget.setCurrentIndex(0)

        self.search_for.completer().setCaseSensitivity(Qt.CaseSensitive)
        self.replace_with.completer().setCaseSensitivity(Qt.CaseSensitive)
        self.s_r_template.completer().setCaseSensitivity(Qt.CaseSensitive)

        self.s_r_search_mode_changed(self.search_mode.currentIndex())
        self.multiple_separator.setFixedWidth(30)
        self.multiple_separator.setText(' ::: ')
        self.multiple_separator.textChanged.connect(self.s_r_separator_changed)
        self.results_count.valueChanged[int].connect(self.s_r_display_bounds_changed)
        self.starting_from.valueChanged[int].connect(self.s_r_display_bounds_changed)

        self.save_button.clicked.connect(self.s_r_save_query)
        self.remove_button.clicked.connect(self.s_r_remove_query)

        self.queries = JSONConfig("search_replace_queries")
        self.saved_search_name = ''
        self.query_field.addItem("")
        self.query_field_values = sorted([q for q in self.queries], key=sort_key)
        self.query_field.addItems(self.query_field_values)
        self.query_field.currentIndexChanged[str].connect(self.s_r_query_change)
        self.query_field.setCurrentIndex(0)
        self.search_field.setCurrentIndex(0)
        self.s_r_search_field_changed(0)

    def s_r_sf_itemdata(self, idx):
        if idx is None:
            idx = self.search_field.currentIndex()
        return unicode(self.search_field.itemData(idx).toString())

    def s_r_df_itemdata(self, idx):
        if idx is None:
            idx = self.destination_field.currentIndex()
        return unicode(self.destination_field.itemData(idx).toString())

    def s_r_get_field(self, mi, field):
        if field:
            if field == '{template}':
                v = SafeFormat().safe_format(
                    unicode(self.s_r_template.text()), mi, _('S/R TEMPLATE ERROR'), mi)
                return [v]
            fm = self.db.metadata_for_field(field)
            if field == 'sort':
                val = mi.get('title_sort', None)
            elif fm['datatype'] == 'datetime':
                val = mi.format_field(field)[1]
            else:
                val = mi.get(field, None)
            if isinstance(val, (int, float, bool)):
                val = str(val)
            elif fm['is_csp']:
                # convert the csp dict into a list
                id_type = unicode(self.s_r_src_ident.currentText())
                if id_type:
                    val = [val.get(id_type, '')]
                else:
                    val = [u'%s:%s'%(t[0], t[1]) for t in val.iteritems()]
            if val is None:
                val = [] if fm['is_multiple'] else ['']
            elif not fm['is_multiple']:
                val = [val]
            elif fm['datatype'] == 'composite':
                val = [v2.strip() for v2 in val.split(fm['is_multiple']['ui_to_list'])]
            elif field == 'authors':
                val = [v2.replace('|', ',') for v2 in val]
        else:
            val = []
        if not val:
            val = ['']
        return val

    def s_r_display_bounds_changed(self, i):
        self.s_r_search_field_changed(self.search_field.currentIndex())

    def s_r_template_changed(self):
        self.s_r_search_field_changed(self.search_field.currentIndex())

    def s_r_identifier_type_changed(self, idx):
        self.s_r_search_field_changed(self.search_field.currentIndex())
        self.s_r_paint_results(idx)

    def s_r_search_field_changed(self, idx):
        self.s_r_template.setVisible(False)
        self.template_label.setVisible(False)
        self.s_r_src_ident_label.setVisible(False)
        self.s_r_src_ident.setVisible(False)
        if idx == 1:  # Template
            self.s_r_template.setVisible(True)
            self.template_label.setVisible(True)
        elif self.s_r_sf_itemdata(idx) == 'identifiers':
            self.s_r_src_ident_label.setVisible(True)
            self.s_r_src_ident.setVisible(True)

        for i in range(0, self.s_r_number_of_books):
            w = getattr(self, 'book_%d_text'%(i+1))
            mi = self.db.get_metadata(self.ids[i], index_is_id=True)
            src = self.s_r_sf_itemdata(idx)
            t = self.s_r_get_field(mi, src)
            if len(t) > 1:
                t = t[self.starting_from.value()-1:
                      self.starting_from.value()-1 + self.results_count.value()]
            w.setText(unicode(self.multiple_separator.text()).join(t))

        if self.search_mode.currentIndex() == 0:
            self.destination_field.setCurrentIndex(idx)
        else:
            self.s_r_destination_field_changed(self.destination_field.currentIndex())
            self.s_r_paint_results(None)

    def s_r_destination_field_changed(self, idx):
        self.s_r_dst_ident_label.setVisible(False)
        self.s_r_dst_ident.setVisible(False)
        txt = self.s_r_df_itemdata(idx)
        if not txt:
            txt = self.s_r_sf_itemdata(None)
        if txt and txt in self.writable_fields:
            if txt == 'identifiers':
                self.s_r_dst_ident_label.setVisible(True)
                self.s_r_dst_ident.setVisible(True)
            self.destination_field_fm = self.db.metadata_for_field(txt)
        self.s_r_paint_results(None)

    def s_r_search_mode_changed(self, val):
        self.search_field.clear()
        self.destination_field.clear()
        if val == 0:
            for f in self.writable_fields:
                self.search_field.addItem(f if f != 'sort' else 'title_sort', f)
                self.destination_field.addItem(f if f != 'sort' else 'title_sort', f)
            self.destination_field.setCurrentIndex(0)
            self.destination_field.setVisible(False)
            self.destination_field_label.setVisible(False)
            self.replace_mode.setCurrentIndex(0)
            self.replace_mode.setVisible(False)
            self.replace_mode_label.setVisible(False)
            self.comma_separated.setVisible(False)
            self.s_r_heading.setText('<p>'+self.main_heading + self.character_heading)
        else:
            self.search_field.blockSignals(True)
            self.destination_field.blockSignals(True)
            for f in self.all_fields:
                self.search_field.addItem(f if f != 'sort' else 'title_sort', f)
            for f in self.writable_fields:
                self.destination_field.addItem(f if f != 'sort' else 'title_sort', f)
            self.search_field.blockSignals(False)
            self.destination_field.blockSignals(False)
            self.destination_field.setVisible(True)
            self.destination_field_label.setVisible(True)
            self.replace_mode.setVisible(True)
            self.replace_mode_label.setVisible(True)
            self.comma_separated.setVisible(True)
            self.s_r_heading.setText('<p>'+self.main_heading + self.regexp_heading)
        self.s_r_paint_results(None)

    def s_r_separator_changed(self, txt):
        self.s_r_search_field_changed(self.search_field.currentIndex())

    def s_r_set_colors(self):
        if self.s_r_error is not None:
            col = 'rgb(255, 0, 0, 20%)'
            self.test_result.setText(self.s_r_error.message)
        else:
            col = 'rgb(0, 255, 0, 20%)'
        self.test_result.setStyleSheet('QLineEdit { color: black; '
                                       'background-color: %s; }'%col)
        for i in range(0,self.s_r_number_of_books):
            getattr(self, 'book_%d_result'%(i+1)).setText('')

    def s_r_func(self, match):
        rfunc = self.s_r_functions[unicode(self.replace_func.currentText())]
        rtext = unicode(self.replace_with.text())
        rtext = match.expand(rtext)
        return rfunc(rtext)

    def s_r_do_regexp(self, mi):
        src_field = self.s_r_sf_itemdata(None)
        src = self.s_r_get_field(mi, src_field)
        result = []
        rfunc = self.s_r_functions[unicode(self.replace_func.currentText())]
        for s in src:
            t = self.s_r_obj.sub(self.s_r_func, s)
            if self.search_mode.currentIndex() == 0:
                t = rfunc(t)
            result.append(t)
        return result

    def s_r_do_destination(self, mi, val):
        src = self.s_r_sf_itemdata(None)
        if src == '':
            return ''
        dest = self.s_r_df_itemdata(None)
        if dest == '':
            if (src == '{template}' or
                        self.db.metadata_for_field(src)['datatype'] == 'composite'):
                raise Exception(_('You must specify a destination when source is '
                                  'a composite field or a template'))
            dest = src
        dest_mode = self.replace_mode.currentIndex()

        if self.destination_field_fm['is_csp']:
            dest_ident = unicode(self.s_r_dst_ident.text())
            if not dest_ident or (src == 'identifiers' and dest_ident == '*'):
                raise Exception(_('You must specify a destination identifier type'))

        if self.destination_field_fm['is_multiple']:
            if self.comma_separated.isChecked():
                splitter = self.destination_field_fm['is_multiple']['ui_to_list']
                res = []
                for v in val:
                    res.extend([x.strip() for x in v.split(splitter) if x.strip()])
                val = res
            else:
                val = [v.replace(',', '') for v in val]

        if dest_mode != 0:
            dest_val = mi.get(dest, '')
            if self.db.metadata_for_field(dest)['is_csp']:
                dst_id_type = unicode(self.s_r_dst_ident.text())
                if dst_id_type:
                    dest_val = [dest_val.get(dst_id_type, '')]
                else:
                    # convert the csp dict into a list
                    dest_val = [u'%s:%s'%(t[0], t[1]) for t in dest_val.iteritems()]
            if dest_val is None:
                dest_val = []
            elif not isinstance(dest_val, list):
                dest_val = [dest_val]
        else:
            dest_val = []

        if dest_mode == 1:
            val.extend(dest_val)
        elif dest_mode == 2:
            val[0:0] = dest_val
        return val

    def s_r_replace_mode_separator(self):
        if self.comma_separated.isChecked():
            return ','
        return ''

    def s_r_paint_results(self, txt):
        self.s_r_error = None
        self.s_r_set_colors()

        if self.case_sensitive.isChecked():
            flags = 0
        else:
            flags = re.I

        flags |= re.UNICODE

        try:
            stext = unicode(self.search_for.text())
            if not stext:
                raise Exception(_('You must specify a search expression in the "Search for" field'))
            if self.search_mode.currentIndex() == 0:
                self.s_r_obj = re.compile(re.escape(stext), flags)
            else:
                self.s_r_obj = re.compile(stext, flags)
        except Exception as e:
            self.s_r_obj = None
            self.s_r_error = e
            self.s_r_set_colors()
            return

        try:
            self.test_result.setText(self.s_r_obj.sub(self.s_r_func,
                                     unicode(self.test_text.text())))
        except Exception as e:
            self.s_r_error = e
            self.s_r_set_colors()
            return

        for i in range(0,self.s_r_number_of_books):
            mi = self.db.get_metadata(self.ids[i], index_is_id=True)
            wr = getattr(self, 'book_%d_result'%(i+1))
            try:
                result = self.s_r_do_regexp(mi)
                t = self.s_r_do_destination(mi, result)
                if len(t) > 1 and self.destination_field_fm['is_multiple']:
                    t = t[self.starting_from.value()-1:
                          self.starting_from.value()-1 + self.results_count.value()]
                    t = unicode(self.multiple_separator.text()).join(t)
                else:
                    t = self.s_r_replace_mode_separator().join(t)
                wr.setText(t)
            except Exception as e:
                self.s_r_error = e
                self.s_r_set_colors()
                break

    def do_search_replace(self, book_id):
        source = self.s_r_sf_itemdata(None)
        if not source or not self.s_r_obj:
            return
        dest = self.s_r_df_itemdata(None)
        if not dest:
            dest = source
        dfm = self.db.field_metadata[dest]
        mi = self.db.new_api.get_proxy_metadata(book_id)
        val = self.s_r_do_regexp(mi)
        val = self.s_r_do_destination(mi, val)
        if dfm['is_multiple']:
            if dfm['is_csp']:
                # convert the colon-separated pair strings back into a dict,
                # which is what set_identifiers wants
                dst_id_type = unicode(self.s_r_dst_ident.text())
                if dst_id_type and dst_id_type != '*':
                    v = ''.join(val)
                    ids = mi.get(dest)
                    ids[dst_id_type] = v
                    val = ids
                else:
                    try:
                        val = dict([(t.split(':')) for t in val])
                    except:
                        raise Exception(_('Invalid identifier string. It must be a '
                                          'comma-separated list of pairs of '
                                          'strings separated by a colon'))
        else:
            val = self.s_r_replace_mode_separator().join(val)
            if dest == 'title' and len(val) == 0:
                val = _('Unknown')

        self.set_field_calls[dest][book_id] = val
    # }}}

    def create_custom_column_editors(self):
        w = self.central_widget.widget(1)
        layout = QGridLayout()
        self.custom_column_widgets, self.__cc_spacers = \
            populate_metadata_page(layout, self.db, self.ids, parent=w,
                                   two_column=False, bulk=True)
        w.setLayout(layout)
        self.__custom_col_layouts = [layout]
        ans = self.custom_column_widgets
        for i in range(len(ans)-1):
            w.setTabOrder(ans[i].widgets[-1], ans[i+1].widgets[1])
            for c in range(2, len(ans[i].widgets), 2):
                w.setTabOrder(ans[i].widgets[c-1], ans[i].widgets[c+1])

    def initialize_combos(self):
        self.initalize_authors()
        self.initialize_series()
        self.initialize_publisher()
        for x in ('authors', 'publisher', 'series'):
            x = getattr(self, x)
            x.setSizeAdjustPolicy(x.AdjustToMinimumContentsLengthWithIcon)
            x.setMinimumContentsLength(25)

    def initalize_authors(self):
        all_authors = self.db.all_authors()
        all_authors.sort(key=lambda x : sort_key(x[1]))

        self.authors.set_separator('&')
        self.authors.set_space_before_sep(True)
        self.authors.set_add_separator(tweaks['authors_completer_append_separator'])
        self.authors.update_items_cache(self.db.all_author_names())
        self.authors.show_initial_value('')

    def initialize_series(self):
        all_series = self.db.all_series()
        all_series.sort(key=lambda x : sort_key(x[1]))
        self.series.set_separator(None)
        self.series.update_items_cache([x[1] for x in all_series])
        self.series.show_initial_value('')

    def initialize_publisher(self):
        all_publishers = self.db.all_publishers()
        all_publishers.sort(key=lambda x : sort_key(x[1]))
        self.publisher.set_separator(None)
        self.publisher.update_items_cache([x[1] for x in all_publishers])
        self.publisher.show_initial_value('')

    def tag_editor(self, *args):
        d = TagEditor(self, self.db, None)
        d.exec_()
        if d.result() == QDialog.Accepted:
            tag_string = ', '.join(d.tags)
            self.tags.setText(tag_string)
            self.tags.update_items_cache(self.db.all_tags())
            self.remove_tags.update_items_cache(self.db.all_tags())

    def auto_number_changed(self, state):
        if state:
            self.series_numbering_restarts.setEnabled(True)
            self.series_start_number.setEnabled(True)
        else:
            self.series_numbering_restarts.setEnabled(False)
            self.series_numbering_restarts.setChecked(False)
            self.series_start_number.setEnabled(False)
            self.series_start_number.setValue(1)

    def reject(self):
        self.save_state()
        ResizableDialog.reject(self)

    def accept(self):
        self.save_state()
        if len(self.ids) < 1:
            return QDialog.accept(self)
        try:
            source = self.s_r_sf_itemdata(None)
        except:
            source = ''
        do_sr = source and self.s_r_obj

        if self.s_r_error is not None and do_sr:
            error_dialog(self, _('Search/replace invalid'),
                    _('Search pattern is invalid: %s')%self.s_r_error.message,
                    show=True)
            return False
        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_all = self.remove_all_tags.isChecked()
        remove = []
        if not remove_all:
            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
        clear_series = self.clear_series.isChecked()
        clear_pub = self.clear_pub.isChecked()
        series = unicode(self.series.currentText()).strip()
        do_autonumber = self.autonumber_series.isChecked()
        do_series_restart = self.series_numbering_restarts.isChecked()
        series_start_value = self.series_start_number.value()
        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()
        do_title_case = self.change_title_to_title_case.isChecked()
        do_title_sort = self.update_title_sort.isChecked()
        clear_languages = self.clear_languages.isChecked()
        restore_original = self.restore_original.isChecked()
        languages = self.languages.lang_codes
        pubdate = adddate = None
        if self.apply_pubdate.isChecked():
            pubdate = qt_to_dt(self.pubdate.dateTime())
        if self.apply_adddate.isChecked():
            adddate = qt_to_dt(self.adddate.dateTime())

        cover_action = None
        if self.cover_remove.isChecked():
            cover_action = 'remove'
        elif self.cover_generate.isChecked():
            cover_action = 'generate'
        elif self.cover_from_fmt.isChecked():
            cover_action = 'fromfmt'
        elif self.cover_trim.isChecked():
            cover_action = 'trim'
        elif self.cover_clone.isChecked():
            cover_action = 'clone'

        args = Settings(remove_all, 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, do_series_restart,
                series_start_value, do_title_case, cover_action, clear_series, clear_pub,
                pubdate, adddate, do_title_sort, languages, clear_languages,
                restore_original, self.comments)

        self.set_field_calls = defaultdict(dict)
        bb = MyBlockingBusy(args, self.ids, self.db, self.refresh_books,
            getattr(self, 'custom_column_widgets', []),
            self.do_search_replace, do_sr, self.set_field_calls, parent=self)

        # The metadata backup thread causes database commits
        # which can slow down bulk editing of large numbers of books
        self.model.stop_metadata_backup()
        try:
            bb.exec_()
        finally:
            self.model.start_metadata_backup()

        bb.thread = bb.db = bb.cc_widgets = None

        if bb.error is not None:
            return error_dialog(self, _('Failed'),
                    bb.error[0], det_msg=bb.error[1],
                    show=True)

        dynamic['s_r_search_mode'] = self.search_mode.currentIndex()
        self.db.clean()
        return QDialog.accept(self)

    def series_changed(self, *args):
        self.write_series = bool(unicode(self.series.currentText()).strip())
        self.autonumber_series.setEnabled(True)

    def s_r_remove_query(self, *args):
        if self.query_field.currentIndex() == 0:
            return

        if not question_dialog(self, _("Delete saved search/replace"),
                _("The selected saved search/replace will be deleted. "
                    "Are you sure?")):
            return

        item_id = self.query_field.currentIndex()
        item_name = unicode(self.query_field.currentText())

        self.query_field.blockSignals(True)
        self.query_field.removeItem(item_id)
        self.query_field.blockSignals(False)
        self.query_field.setCurrentIndex(0)

        if item_name in self.queries.keys():
            del(self.queries[item_name])
            self.queries.commit()

    def s_r_save_query(self, *args):
        names = ['']
        names.extend(self.query_field_values)
        try:
            dex = names.index(self.saved_search_name)
        except:
            dex = 0
        name = ''
        while not name:
            name, ok =  QInputDialog.getItem(self, _('Save search/replace'),
                    _('Search/replace name:'), names, dex, True)
            if not ok:
                return
            if not name:
                error_dialog(self, _("Save search/replace"),
                        _("You must provide a name."), show=True)
        new = True
        name = unicode(name)
        if name in self.queries.keys():
            if not question_dialog(self, _("Save search/replace"),
                    _("That saved search/replace already exists and will be overwritten. "
                        "Are you sure?")):
                return
            new = False

        query = {}
        query['name'] = name
        query['search_field'] = unicode(self.search_field.currentText())
        query['search_mode'] = unicode(self.search_mode.currentText())
        query['s_r_template'] = unicode(self.s_r_template.text())
        query['s_r_src_ident'] = unicode(self.s_r_src_ident.currentText())
        query['search_for'] = unicode(self.search_for.text())
        query['case_sensitive'] = self.case_sensitive.isChecked()
        query['replace_with'] = unicode(self.replace_with.text())
        query['replace_func'] = unicode(self.replace_func.currentText())
        query['destination_field'] = unicode(self.destination_field.currentText())
        query['s_r_dst_ident'] = unicode(self.s_r_dst_ident.text())
        query['replace_mode'] = unicode(self.replace_mode.currentText())
        query['comma_separated'] = self.comma_separated.isChecked()
        query['results_count'] = self.results_count.value()
        query['starting_from'] = self.starting_from.value()
        query['multiple_separator'] = unicode(self.multiple_separator.text())

        self.queries[name] = query
        self.queries.commit()

        if new:
            self.query_field.blockSignals(True)
            self.query_field.clear()
            self.query_field.addItem('')
            self.query_field_values = sorted([q for q in self.queries], key=sort_key)
            self.query_field.addItems(self.query_field_values)
            self.query_field.blockSignals(False)
        self.query_field.setCurrentIndex(self.query_field.findText(name))

    def s_r_query_change(self, item_name):
        if not item_name:
            self.s_r_reset_query_fields()
            self.saved_search_name = ''
            return
        item = self.queries.get(unicode(item_name), None)
        if item is None:
            self.s_r_reset_query_fields()
            return
        self.saved_search_name = item_name

        def set_text(attr, key):
            try:
                attr.setText(item[key])
            except:
                pass

        def set_checked(attr, key):
            try:
                attr.setChecked(item[key])
            except:
                attr.setChecked(False)

        def set_value(attr, key):
            try:
                attr.setValue(int(item[key]))
            except:
                attr.setValue(0)

        def set_index(attr, key):
            try:
                attr.setCurrentIndex(attr.findText(item[key]))
            except:
                attr.setCurrentIndex(0)

        set_index(self.search_mode, 'search_mode')
        set_index(self.search_field, 'search_field')
        set_text(self.s_r_template, 's_r_template')

        self.s_r_template_changed()  # simulate gain/loss of focus

        set_index(self.s_r_src_ident, 's_r_src_ident')
        set_text(self.s_r_dst_ident, 's_r_dst_ident')
        set_text(self.search_for, 'search_for')
        set_checked(self.case_sensitive, 'case_sensitive')
        set_text(self.replace_with, 'replace_with')
        set_index(self.replace_func, 'replace_func')
        set_index(self.destination_field, 'destination_field')
        set_index(self.replace_mode, 'replace_mode')
        set_checked(self.comma_separated, 'comma_separated')
        set_value(self.results_count, 'results_count')
        set_value(self.starting_from, 'starting_from')
        set_text(self.multiple_separator, 'multiple_separator')

    def s_r_reset_query_fields(self):
        # Don't reset the search mode. The user will probably want to use it
        # as it was
        self.search_field.setCurrentIndex(0)
        self.s_r_src_ident.setCurrentIndex(0)
        self.s_r_template.setText("")
        self.search_for.setText("")
        self.case_sensitive.setChecked(False)
        self.replace_with.setText("")
        self.replace_func.setCurrentIndex(0)
        self.destination_field.setCurrentIndex(0)
        self.s_r_dst_ident.setText('')
        self.replace_mode.setCurrentIndex(0)
        self.comma_separated.setChecked(True)
        self.results_count.setValue(999)
        self.starting_from.setValue(1)
        self.multiple_separator.setText(" ::: ")
Example #43
0
def load_themes():
    return JSONConfig('viewer_themes')
Example #44
0
    def prepare_search_and_replace(self):
        self.search_for.initialize('bulk_edit_search_for')
        self.replace_with.initialize('bulk_edit_replace_with')
        self.s_r_template.initialize('bulk_edit_template')
        self.test_text.initialize('bulk_edit_test_test')
        self.all_fields = ['']
        self.writable_fields = ['']
        fm = self.db.field_metadata
        for f in fm:
            if (f in ['author_sort'] or
                    (fm[f]['datatype'] in ['text', 'series', 'enumeration', 'comments']
                     and fm[f].get('search_terms', None)
                     and f not in ['formats', 'ondevice', 'series_sort']) or
                    (fm[f]['datatype'] in ['int', 'float', 'bool', 'datetime'] and
                     f not in ['id', 'timestamp'])):
                self.all_fields.append(f)
                self.writable_fields.append(f)
            if fm[f]['datatype'] == 'composite':
                self.all_fields.append(f)
        self.all_fields.sort()
        self.all_fields.insert(1, '{template}')
        self.writable_fields.sort()
        self.search_field.setMaxVisibleItems(25)
        self.destination_field.setMaxVisibleItems(25)
        self.testgrid.setColumnStretch(1, 1)
        self.testgrid.setColumnStretch(2, 1)
        offset = 10
        self.s_r_number_of_books = min(10, len(self.ids))
        for i in range(1,self.s_r_number_of_books+1):
            w = QLabel(self.tabWidgetPage3)
            w.setText(_('Book %d:')%i)
            self.testgrid.addWidget(w, i+offset, 0, 1, 1)
            w = QLineEdit(self.tabWidgetPage3)
            w.setReadOnly(True)
            name = 'book_%d_text'%i
            setattr(self, name, w)
            self.book_1_text.setObjectName(name)
            self.testgrid.addWidget(w, i+offset, 1, 1, 1)
            w = QLineEdit(self.tabWidgetPage3)
            w.setReadOnly(True)
            name = 'book_%d_result'%i
            setattr(self, name, w)
            self.book_1_text.setObjectName(name)
            self.testgrid.addWidget(w, i+offset, 2, 1, 1)

        ident_types = sorted(self.db.get_all_identifier_types(), key=sort_key)
        self.s_r_dst_ident.setCompleter(QCompleter(ident_types))
        try:
            self.s_r_dst_ident.setPlaceholderText(_('Enter an identifier type'))
        except:
            pass
        self.s_r_src_ident.addItems(ident_types)

        self.main_heading = _(
                 '<b>You can destroy your library using this feature.</b> '
                 'Changes are permanent. There is no undo function. '
                 'You are strongly encouraged to back up your library '
                 'before proceeding.<p>'
                 'Search and replace in text fields using character matching '
                 'or regular expressions. ')

        self.character_heading = _(
                 'In character mode, the field is searched for the entered '
                 'search text. The text is replaced by the specified replacement '
                 'text everywhere it is found in the specified field. After '
                 'replacement is finished, the text can be changed to '
                 'upper-case, lower-case, or title-case. If the case-sensitive '
                 'check box is checked, the search text must match exactly. If '
                 'it is unchecked, the search text will match both upper- and '
                 'lower-case letters'
                 )

        self.regexp_heading = _(
                 'In regular expression mode, the search text is an '
                 'arbitrary python-compatible regular expression. The '
                 'replacement text can contain backreferences to parenthesized '
                 'expressions in the pattern. The search is not anchored, '
                 'and can match and replace multiple times on the same string. '
                 'The modification functions (lower-case etc) are applied to the '
                 'matched text, not to the field as a whole. '
                 'The destination box specifies the field where the result after '
                 'matching and replacement is to be assigned. You can replace '
                 'the text in the field, or prepend or append the matched text. '
                 'See <a href="http://docs.python.org/library/re.html"> '
                 'this reference</a> for more information on python\'s regular '
                 'expressions, and in particular the \'sub\' function.'
                 )

        self.search_mode.addItems(self.s_r_match_modes)
        self.search_mode.setCurrentIndex(dynamic.get('s_r_search_mode', 0))
        self.replace_mode.addItems(self.s_r_replace_modes)
        self.replace_mode.setCurrentIndex(0)

        self.s_r_search_mode = 0
        self.s_r_error = None
        self.s_r_obj = None

        self.replace_func.addItems(sorted(self.s_r_functions.keys()))
        self.search_mode.currentIndexChanged[int].connect(self.s_r_search_mode_changed)
        self.search_field.currentIndexChanged[int].connect(self.s_r_search_field_changed)
        self.destination_field.currentIndexChanged[int].connect(self.s_r_destination_field_changed)

        self.replace_mode.currentIndexChanged[int].connect(self.s_r_paint_results)
        self.replace_func.currentIndexChanged[str].connect(self.s_r_paint_results)
        self.search_for.editTextChanged[str].connect(self.s_r_paint_results)
        self.replace_with.editTextChanged[str].connect(self.s_r_paint_results)
        self.test_text.editTextChanged[str].connect(self.s_r_paint_results)
        self.comma_separated.stateChanged.connect(self.s_r_paint_results)
        self.case_sensitive.stateChanged.connect(self.s_r_paint_results)
        self.s_r_src_ident.currentIndexChanged[int].connect(self.s_r_identifier_type_changed)
        self.s_r_dst_ident.textChanged.connect(self.s_r_paint_results)
        self.s_r_template.lost_focus.connect(self.s_r_template_changed)
        self.central_widget.setCurrentIndex(0)

        self.search_for.completer().setCaseSensitivity(Qt.CaseSensitive)
        self.replace_with.completer().setCaseSensitivity(Qt.CaseSensitive)
        self.s_r_template.completer().setCaseSensitivity(Qt.CaseSensitive)

        self.s_r_search_mode_changed(self.search_mode.currentIndex())
        self.multiple_separator.setFixedWidth(30)
        self.multiple_separator.setText(' ::: ')
        self.multiple_separator.textChanged.connect(self.s_r_separator_changed)
        self.results_count.valueChanged[int].connect(self.s_r_display_bounds_changed)
        self.starting_from.valueChanged[int].connect(self.s_r_display_bounds_changed)

        self.save_button.clicked.connect(self.s_r_save_query)
        self.remove_button.clicked.connect(self.s_r_remove_query)

        self.queries = JSONConfig("search_replace_queries")
        self.saved_search_name = ''
        self.query_field.addItem("")
        self.query_field_values = sorted([q for q in self.queries], key=sort_key)
        self.query_field.addItems(self.query_field_values)
        self.query_field.currentIndexChanged[str].connect(self.s_r_query_change)
        self.query_field.setCurrentIndex(0)
        self.search_field.setCurrentIndex(0)
        self.s_r_search_field_changed(0)
Example #45
0
from calibre.gui2.viewer.toc import TOC
from calibre.gui2.widgets import ProgressIndicator
from calibre.gui2 import (
    Application, ORG_NAME, APP_UID, choose_files, info_dialog, error_dialog,
    open_url, setup_gui_option_parser)
from calibre.ebooks.oeb.iterator.book import EbookIterator
from calibre.constants import islinux, filesystem_encoding
from calibre.utils.config import Config, StringConfig, JSONConfig
from calibre.customize.ui import available_input_formats
from calibre import as_unicode, force_unicode, isbytestring, prints
from calibre.ptempfile import reset_base_dir
from calibre.utils.ipc import viewer_socket_address, RC
from calibre.utils.zipfile import BadZipfile
from calibre.utils.localization import canonicalize_lang, lang_as_iso639_1, get_lang

vprefs = JSONConfig('viewer')
vprefs.defaults['singleinstance'] = False
dprefs = JSONConfig('viewer_dictionaries')
dprefs.defaults['word_lookups'] = {}
singleinstance_name = 'calibre_viewer'

class Worker(Thread):

    def run(self):
        from calibre.utils.ipc.simple_worker import WorkerError
        try:
            Thread.run(self)
            self.exception = self.traceback = None
        except BadZipfile:
            self.exception = _(
                'This ebook is corrupted and cannot be opened. If you '
Example #46
0
    QSize, Qt, QApplication, QIcon)

from calibre.ebooks.oeb.polish.utils import apply_func_to_match_groups, apply_func_to_html_text
from calibre.gui2 import error_dialog
from calibre.gui2.complete2 import EditWithComplete
from calibre.gui2.tweak_book import dictionaries
from calibre.gui2.tweak_book.widgets import Dialog
from calibre.gui2.tweak_book.editor.text import TextEdit
from calibre.utils.config import JSONConfig
from calibre.utils.icu import capitalize, upper, lower, swapcase
from calibre.utils.titlecase import titlecase
from calibre.utils.localization import localize_user_manual_link
from polyglot.builtins import iteritems, unicode_type
from polyglot.io import PolyglotStringIO

user_functions = JSONConfig('editor-search-replace-functions')


def compile_code(src, name='<string>'):
    if not isinstance(src, unicode_type):
        match = re.search(br'coding[:=]\s*([-\w.]+)', src[:200])
        enc = match.group(1).decode('utf-8') if match else 'utf-8'
        src = src.decode(enc)
    if not src or not src.strip():
        src = EMPTY_FUNC
    # Python complains if there is a coding declaration in a unicode string
    src = re.sub(r'^#.*coding\s*[:=]\s*([-\w.]+)', '#', src, flags=re.MULTILINE)
    # Translate newlines to \n
    src = io.StringIO(src, newline=None).getvalue()
    code = compile(src, name, 'exec')
Example #47
0
    def run(self, path_to_output, opts, db, notification=DummyReporter()):
        from calibre.library.catalogs.epub_mobi_builder import CatalogBuilder
        from calibre.utils.logging import default_log as log
        from calibre.utils.config import JSONConfig

        # If preset specified from the cli, insert stored options from JSON file
        if hasattr(opts, 'preset') and opts.preset:
            available_presets = JSONConfig("catalog_presets")
            if opts.preset not in available_presets:
                if available_presets:
                    print(_('Error: Preset "%s" not found.' % opts.preset))
                    print(_('Stored presets: %s' % ', '.join([p for p in sorted(available_presets.keys())])))
                else:
                    print(_('Error: No stored presets.'))
                return 1

            # Copy the relevant preset values to the opts object
            for item in available_presets[opts.preset]:
                if item not in ['exclusion_rules_tw', 'format', 'prefix_rules_tw']:
                    setattr(opts, item, available_presets[opts.preset][item])

            # Provide an unconnected device
            opts.connected_device = {
                         'is_device_connected': False,
                         'kind': None,
                         'name': None,
                         'save_template': None,
                         'serial': None,
                         'storage': None,
                        }

            # Convert prefix_rules and exclusion_rules from JSON lists to tuples
            prs = []
            for rule in opts.prefix_rules:
                prs.append(tuple(rule))
            opts.prefix_rules = tuple(prs)

            ers = []
            for rule in opts.exclusion_rules:
                ers.append(tuple(rule))
            opts.exclusion_rules = tuple(ers)

        opts.log = log
        opts.fmt = self.fmt = path_to_output.rpartition('.')[2]

        # Add local options
        opts.creator = '%s, %s %s, %s' % (strftime('%A'), strftime('%B'), strftime('%d').lstrip('0'), strftime('%Y'))
        opts.creator_sort_as = '%s %s' % ('calibre', strftime('%Y-%m-%d'))
        opts.connected_kindle = False

        # Finalize output_profile
        op = opts.output_profile
        if op is None:
            op = 'default'

        if opts.connected_device['name'] and 'kindle' in opts.connected_device['name'].lower():
            opts.connected_kindle = True
            if opts.connected_device['serial'] and \
               opts.connected_device['serial'][:4] in ['B004', 'B005']:
                op = "kindle_dx"
            else:
                op = "kindle"

        opts.description_clip = 380 if op.endswith('dx') or 'kindle' not in op else 100
        opts.author_clip = 100 if op.endswith('dx') or 'kindle' not in op else 60
        opts.output_profile = op

        opts.basename = "Catalog"
        opts.cli_environment = not hasattr(opts, 'sync')

        # Hard-wired to always sort descriptions by author, with series after non-series
        opts.sort_descriptions_by_author = True

        build_log = []

        build_log.append(u"%s('%s'): Generating %s %sin %s environment, locale: '%s'" %
            (self.name,
             current_library_name(),
             self.fmt,
             'for %s ' % opts.output_profile if opts.output_profile else '',
             'CLI' if opts.cli_environment else 'GUI',
             calibre_langcode_to_name(canonicalize_lang(get_lang()), localize=False))
             )

        # If exclude_genre is blank, assume user wants all tags as genres
        if opts.exclude_genre.strip() == '':
            # opts.exclude_genre = '\[^.\]'
            # build_log.append(" converting empty exclude_genre to '\[^.\]'")
            opts.exclude_genre = 'a^'
            build_log.append(" converting empty exclude_genre to 'a^'")
        if opts.connected_device['is_device_connected'] and \
           opts.connected_device['kind'] == 'device':
            if opts.connected_device['serial']:
                build_log.append(u" connected_device: '%s' #%s%s " %
                    (opts.connected_device['name'],
                     opts.connected_device['serial'][0:4],
                     'x' * (len(opts.connected_device['serial']) - 4)))
                for storage in opts.connected_device['storage']:
                    if storage:
                        build_log.append(u"  mount point: %s" % storage)
            else:
                build_log.append(u" connected_device: '%s'" % opts.connected_device['name'])
                try:
                    for storage in opts.connected_device['storage']:
                        if storage:
                            build_log.append(u"  mount point: %s" % storage)
                except:
                    build_log.append(u"  (no mount points)")
        else:
            build_log.append(u" connected_device: '%s'" % opts.connected_device['name'])

        opts_dict = vars(opts)
        if opts_dict['ids']:
            build_log.append(" book count: %d" % len(opts_dict['ids']))

        sections_list = []
        if opts.generate_authors:
            sections_list.append('Authors')
        if opts.generate_titles:
            sections_list.append('Titles')
        if opts.generate_series:
            sections_list.append('Series')
        if opts.generate_genres:
            sections_list.append('Genres')
        if opts.generate_recently_added:
            sections_list.append('Recently Added')
        if opts.generate_descriptions:
            sections_list.append('Descriptions')

        if not sections_list:
            if opts.cli_environment:
                opts.log.warn('*** No Section switches specified, enabling all Sections ***')
                opts.generate_authors = True
                opts.generate_titles = True
                opts.generate_series = True
                opts.generate_genres = True
                opts.generate_recently_added = True
                opts.generate_descriptions = True
                sections_list = ['Authors', 'Titles', 'Series', 'Genres', 'Recently Added', 'Descriptions']
            else:
                opts.log.warn('\n*** No enabled Sections, terminating catalog generation ***')
                return ["No Included Sections", "No enabled Sections.\nCheck E-book options tab\n'Included sections'\n"]
        if opts.fmt == 'mobi' and sections_list == ['Descriptions']:
            warning = _("\n*** Adding 'By authors' section required for MOBI output ***")
            opts.log.warn(warning)
            sections_list.insert(0, 'Authors')
            opts.generate_authors = True

        opts.log(u" Sections: %s" % ', '.join(sections_list))
        opts.section_list = sections_list

        # Limit thumb_width to 1.0" - 2.0"
        try:
            if float(opts.thumb_width) < float(self.THUMB_SMALLEST):
                log.warning("coercing thumb_width from '%s' to '%s'" % (opts.thumb_width, self.THUMB_SMALLEST))
                opts.thumb_width = self.THUMB_SMALLEST
            if float(opts.thumb_width) > float(self.THUMB_LARGEST):
                log.warning("coercing thumb_width from '%s' to '%s'" % (opts.thumb_width, self.THUMB_LARGEST))
                opts.thumb_width = self.THUMB_LARGEST
            opts.thumb_width = "%.2f" % float(opts.thumb_width)
        except:
            log.error("coercing thumb_width from '%s' to '%s'" % (opts.thumb_width, self.THUMB_SMALLEST))
            opts.thumb_width = "1.0"

        # eval prefix_rules if passed from command line
        if type(opts.prefix_rules) is not tuple:
            try:
                opts.prefix_rules = eval(opts.prefix_rules)
            except:
                log.error("malformed --prefix-rules: %s" % opts.prefix_rules)
                raise
            for rule in opts.prefix_rules:
                if len(rule) != 4:
                    log.error("incorrect number of args for --prefix-rules: %s" % repr(rule))

        # eval exclusion_rules if passed from command line
        if type(opts.exclusion_rules) is not tuple:
            try:
                opts.exclusion_rules = eval(opts.exclusion_rules)
            except:
                log.error("malformed --exclusion-rules: %s" % opts.exclusion_rules)
                raise
            for rule in opts.exclusion_rules:
                if len(rule) != 3:
                    log.error("incorrect number of args for --exclusion-rules: %s" % repr(rule))

        # Display opts
        keys = sorted(opts_dict.keys())
        build_log.append(" opts:")
        for key in keys:
            if key in ['catalog_title', 'author_clip', 'connected_kindle', 'creator',
                       'cross_reference_authors', 'description_clip', 'exclude_book_marker',
                       'exclude_genre', 'exclude_tags', 'exclusion_rules', 'fmt',
                       'genre_source_field', 'header_note_source_field', 'merge_comments_rule',
                       'output_profile', 'prefix_rules', 'preset', 'read_book_marker',
                       'search_text', 'sort_by', 'sort_descriptions_by_author', 'sync',
                       'thumb_width', 'use_existing_cover', 'wishlist_tag']:
                build_log.append("  %s: %s" % (key, repr(opts_dict[key])))
        if opts.verbose:
            log('\n'.join(line for line in build_log))

        # Capture start_time
        opts.start_time = time.time()

        self.opts = opts

        if opts.verbose:
            log.info(" Begin catalog source generation (%s)" %
                     str(datetime.timedelta(seconds=int(time.time() - opts.start_time))))

        # Launch the Catalog builder
        catalog = CatalogBuilder(db, opts, self, report_progress=notification)

        try:
            catalog.build_sources()
            if opts.verbose:
                log.info(" Completed catalog source generation (%s)\n"  %
                         str(datetime.timedelta(seconds=int(time.time() - opts.start_time))))
        except (AuthorSortMismatchException, EmptyCatalogException) as e:
            log.error(" *** Terminated catalog generation: %s ***" % e)
        except:
            log.error(" unhandled exception in catalog generator")
            raise

        else:
            recommendations = []
            recommendations.append(('remove_fake_margins', False,
                OptionRecommendation.HIGH))
            recommendations.append(('comments', '', OptionRecommendation.HIGH))

            """
            >>> Use to debug generated catalog code before pipeline conversion <<<
            """
            GENERATE_DEBUG_EPUB = False
            if GENERATE_DEBUG_EPUB:
                catalog_debug_path = os.path.join(os.path.expanduser('~'), 'Desktop', 'Catalog debug')
                setattr(opts, 'debug_pipeline', os.path.expanduser(catalog_debug_path))

            dp = getattr(opts, 'debug_pipeline', None)
            if dp is not None:
                recommendations.append(('debug_pipeline', dp,
                    OptionRecommendation.HIGH))

            if opts.output_profile and opts.output_profile.startswith("kindle"):
                recommendations.append(('output_profile', opts.output_profile,
                    OptionRecommendation.HIGH))
                recommendations.append(('book_producer', opts.output_profile,
                    OptionRecommendation.HIGH))
                if opts.fmt == 'mobi':
                    recommendations.append(('no_inline_toc', True,
                        OptionRecommendation.HIGH))
                    recommendations.append(('verbose', 2,
                        OptionRecommendation.HIGH))

            # Use existing cover or generate new cover
            cpath = None
            existing_cover = False
            try:
                search_text = 'title:"%s" author:%s' % (
                        opts.catalog_title.replace('"', '\\"'), 'calibre')
                matches = db.search(search_text, return_matches=True, sort_results=False)
                if matches:
                    cpath = db.cover(matches[0], index_is_id=True, as_path=True)
                    if cpath and os.path.exists(cpath):
                        existing_cover = True
            except:
                pass

            if self.opts.use_existing_cover and not existing_cover:
                log.warning("no existing catalog cover found")

            if self.opts.use_existing_cover and existing_cover:
                recommendations.append(('cover', cpath, OptionRecommendation.HIGH))
                log.info("using existing catalog cover")
            else:
                from calibre.ebooks.covers import calibre_cover2
                log.info("replacing catalog cover")
                new_cover_path = PersistentTemporaryFile(suffix='.jpg')
                new_cover = calibre_cover2(opts.catalog_title, 'calibre')
                new_cover_path.write(new_cover)
                new_cover_path.close()
                recommendations.append(('cover', new_cover_path.name, OptionRecommendation.HIGH))

            # Run ebook-convert
            from calibre.ebooks.conversion.plumber import Plumber
            plumber = Plumber(os.path.join(catalog.catalog_path, opts.basename + '.opf'),
                            path_to_output, log, report_progress=notification,
                            abort_after_input_dump=False)
            plumber.merge_ui_recommendations(recommendations)
            plumber.run()

            try:
                os.remove(cpath)
            except:
                pass

            if GENERATE_DEBUG_EPUB:
                from calibre.ebooks.epub import initialize_container
                from calibre.ebooks.tweak import zip_rebuilder
                from calibre.utils.zipfile import ZipFile
                input_path = os.path.join(catalog_debug_path, 'input')
                epub_shell = os.path.join(catalog_debug_path, 'epub_shell.zip')
                initialize_container(epub_shell, opf_name='content.opf')
                with ZipFile(epub_shell, 'r') as zf:
                    zf.extractall(path=input_path)
                os.remove(epub_shell)
                zip_rebuilder(input_path, os.path.join(catalog_debug_path, 'input.epub'))

            if opts.verbose:
                log.info(" Catalog creation complete (%s)\n" %
                     str(datetime.timedelta(seconds=int(time.time() - opts.start_time))))

        # returns to gui2.actions.catalog:catalog_generated()
        return catalog.error
Example #48
0
from calibre.gui2.widgets import ProgressIndicator
from calibre.gui2 import (
    Application, ORG_NAME, APP_UID, choose_files, info_dialog, error_dialog,
    open_url, setup_gui_option_parser)
from calibre.ebooks.oeb.iterator.book import EbookIterator
from calibre.ebooks import DRMError
from calibre.constants import islinux, filesystem_encoding
from calibre.utils.config import Config, StringConfig, JSONConfig
from calibre.customize.ui import available_input_formats
from calibre import as_unicode, force_unicode, isbytestring
from calibre.ptempfile import reset_base_dir
from calibre.utils.zipfile import BadZipfile
from calibre.utils.localization import canonicalize_lang, lang_as_iso639_1, get_lang

vprefs = JSONConfig('viewer')
dprefs = JSONConfig('viewer_dictionaries')
dprefs.defaults['word_lookups'] = {}

class Worker(Thread):

    def run(self):
        try:
            Thread.run(self)
            self.exception = self.traceback = None
        except BadZipfile:
            self.exception = _(
                'This ebook is corrupted and cannot be opened. If you '
                'downloaded it from somewhere, try downloading it again.')
            self.traceback = ''
        except Exception as err:
            self.exception = err
Example #49
0
from itertools import chain
from functools import partial

from calibre import prints
from calibre.constants import plugins, config_dir
from calibre.spell import parse_lang_code
from calibre.utils.config import JSONConfig
from calibre.utils.icu import capitalize
from calibre.utils.localization import get_lang, get_system_locale

Dictionary = namedtuple('Dictionary', 'primary_locale locales dicpath affpath builtin name id')
LoadedDictionary = namedtuple('Dictionary', 'primary_locale locales obj builtin name id')
hunspell = plugins['hunspell'][0]
if hunspell is None:
    raise RuntimeError('Failed to load hunspell: %s' % plugins['hunspell'][1])
dprefs = JSONConfig('dictionaries/prefs.json')
dprefs.defaults['preferred_dictionaries'] = {}
dprefs.defaults['preferred_locales'] = {}
dprefs.defaults['user_dictionaries'] = [{'name':_('Default'), 'is_active':True, 'words':[]}]
not_present = object()


class UserDictionary(object):

    __slots__ = ('name', 'is_active', 'words')

    def __init__(self, **kwargs):
        self.name = kwargs['name']
        self.is_active = kwargs['is_active']
        self.words = {(w, langcode) for w, langcode in kwargs['words']}
class BookSettings(object):
    '''Holds book specific settings'''
    def __init__(self, database, book_id, connections):
        self._connections = connections

        book_path = database.field_for('path', book_id).replace('/', os.sep)

        self._prefs = JSONConfig(os.path.join(book_path, 'book_settings'),
                                 base_path=LIBRARY)
        self._prefs.setdefault('asin', '')
        self._prefs.setdefault('goodreads_url', '')
        self._prefs.setdefault('aliases', {})
        self._prefs.setdefault('sample_xray', '')
        self._prefs.commit()

        self._title = database.field_for('title', book_id)
        self._author = ' & '.join(database.field_for('authors', book_id))

        self._asin = self._prefs['asin'] if self._prefs['asin'] != '' else None
        self._goodreads_url = self._prefs['goodreads_url']
        self._sample_xray = self._prefs['sample_xray']

        if not self._asin:
            identifiers = database.field_for('identifiers', book_id)
            if 'mobi-asin' in list(identifiers.keys()):
                self._asin = database.field_for(
                    'identifiers', book_id)['mobi-asin'].decode('ascii')
                self._prefs['asin'] = self._asin
            else:
                self._asin = self.search_for_asin_on_amazon(
                    self.title_and_author)
                if self._asin:
                    metadata = database.get_metadata(book_id)
                    identifiers = metadata.get_identifiers()
                    identifiers['mobi-asin'] = self._asin
                    metadata.set_identifiers(identifiers)
                    database.set_metadata(book_id, metadata)
                    self._prefs['asin'] = self._asin

        if self._goodreads_url == '':
            url = None
            if self._asin:
                url = self.search_for_goodreads_url(self._asin)
            if not url and self._title != 'Unknown' and self._author != 'Unknown':
                url = self.search_for_goodreads_url(self.title_and_author)

            if url:
                self._goodreads_url = url
                self._prefs['goodreads_url'] = self._goodreads_url
                if not self._asin:
                    self._asin = self.search_for_asin_on_goodreads(
                        self._goodreads_url)
                    if self._asin:
                        metadata = database.get_metadata(book_id)
                        identifiers = metadata.get_identifiers()
                        identifiers['mobi-asin'] = self._asin
                        metadata.set_identifiers(identifiers)
                        database.set_metadata(book_id, metadata)
                        self._prefs['asin'] = self._asin

        self._aliases = self._prefs['aliases']

        self.save()

    @property
    def prefs(self):
        return self._prefs

    @property
    def asin(self):
        return self._asin

    @asin.setter
    def asin(self, val):
        self._asin = val

    @property
    def sample_xray(self):
        return self._sample_xray

    @sample_xray.setter
    def sample_xray(self, value):
        self._sample_xray = value

    @property
    def title(self):
        return self._title

    @property
    def author(self):
        return self._author

    @property
    def title_and_author(self):
        return '{0} - {1}'.format(self._title, self._author)

    @property
    def goodreads_url(self):
        return self._goodreads_url

    @goodreads_url.setter
    def goodreads_url(self, val):
        self._goodreads_url = val

    @property
    def aliases(self):
        return self._aliases

    def set_aliases(self, label, aliases):
        '''Sets label's aliases to aliases'''

        # 'aliases' is a string containing a comma separated list of aliases.

        # Split it, remove whitespace from each element, drop empty strings (strangely,
        # split only does this if you don't specify a separator)

        # so "" -> []  "foo,bar" and " foo   , bar " -> ["foo", "bar"]
        aliases = [x.strip() for x in aliases.split(",") if x.strip()]
        self._aliases[label] = aliases

    def save(self):
        '''Saves current settings in book's settings file'''
        self._prefs['asin'] = self._asin
        self._prefs['goodreads_url'] = self._goodreads_url
        self._prefs['aliases'] = self._aliases
        self._prefs['sample_xray'] = self._sample_xray

    def search_for_asin_on_amazon(self, query):
        '''Search for book's asin on amazon using given query'''
        query = urlencode({'keywords': query})
        url = '/s/ref=sr_qz_back?sf=qz&rh=i%3Adigital-text%2Cn%3A154606011%2Ck%3A' + query[
            9:] + '&' + query
        try:
            response = open_url(self._connections['amazon'], url)
        except PageDoesNotExist:
            return None

        # check to make sure there are results
        if ('did not match any products' in response
                and 'Did you mean:' not in response
                and 'so we searched in All Departments' not in response):
            return None

        soup = BeautifulSoup(response)
        results = soup.findAll('div', {'id': 'resultsCol'})

        if not results:
            return None

        for result in results:
            if 'Buy now with 1-Click' in str(result):
                asin_search = AMAZON_ASIN_PAT.search(str(result))
                if asin_search:
                    return asin_search.group(1)

        return None

    def search_for_goodreads_url(self, keywords):
        '''Searches for book's goodreads url using given keywords'''
        query = urlencode({'q': keywords})
        try:
            response = open_url(self._connections['goodreads'],
                                '/search?' + query)
        except PageDoesNotExist:
            return None

        # check to make sure there are results
        if 'No results' in response:
            return None

        urlsearch = GOODREADS_URL_PAT.search(response)
        if not urlsearch:
            return None

        # return the full URL with the query parameters removed
        url = 'https://www.goodreads.com' + urlsearch.group(1)
        return urlparse.urlparse(url)._replace(query=None).geturl()

    def search_for_asin_on_goodreads(self, url):
        '''Searches for ASIN of book at given url'''
        book_id_search = BOOK_ID_PAT.search(url)
        if not book_id_search:
            return None

        book_id = book_id_search.group(1)

        try:
            response = open_url(self._connections['goodreads'],
                                '/buttons/glide/' + book_id)
        except PageDoesNotExist:
            return None

        book_asin_search = GOODREADS_ASIN_PAT.search(response)
        if not book_asin_search:
            return None

        return book_asin_search.group(1)

    def update_aliases(self, source, source_type='url'):
        if source_type.lower() == 'url':
            self.update_aliases_from_url(source)
            return
        if source_type.lower() == 'asc':
            self.update_aliases_from_asc(source)
            return
        if source_type.lower() == 'json':
            self.update_aliases_from_json(source)

    def update_aliases_from_asc(self, filename):
        '''Gets aliases from sample x-ray file and expands them if users settings say to do so'''
        cursor = connect(filename).cursor()
        characters = {
            x[1]: [x[1]]
            for x in cursor.execute('SELECT * FROM entity').fetchall()
            if x[3] == 1
        }

        self._aliases = {}
        for alias, fullname in list(auto_expand_aliases(characters).items()):
            if fullname not in list(self._aliases.keys()):
                self._aliases[fullname] = [alias]
                continue
            self._aliases[fullname].append(alias)

    def update_aliases_from_json(self, filename):
        '''Gets aliases from json file'''
        data = json.load(open(filename))
        self._aliases = {
            name: char['aliases']
            for name, char in list(data['characters'].items())
        } if 'characters' in data else {}
        if 'settings' in data:
            self._aliases.update({
                name: setting['aliases']
                for name, setting in list(data['settings'].items())
            })

    def update_aliases_from_url(self, url):
        '''Gets aliases from Goodreads and expands them if users settings say to do so'''
        try:
            goodreads_parser = GoodreadsParser(url,
                                               self._connections['goodreads'],
                                               self._asin)
            goodreads_chars = goodreads_parser.get_characters(1)
            goodreads_settings = goodreads_parser.get_settings(
                len(goodreads_chars))
        except PageDoesNotExist:
            goodreads_chars = {}
            goodreads_settings = {}

        self._aliases = {}
        for char_data in list(goodreads_chars.values()) + list(
                goodreads_settings.values()):
            if char_data['label'] not in list(self._aliases.keys()):
                self._aliases[char_data['label']] = char_data['aliases']
Example #51
0
class Manager(QObject):  # {{{

    def __init__(self, parent=None, config_name='shortcuts/main'):
        QObject.__init__(self, parent)

        self.config = JSONConfig(config_name)
        self.shortcuts = OrderedDict()
        self.keys_map = {}
        self.groups = {}

    def register_shortcut(self, unique_name, name, default_keys=(),
            description=None, action=None, group=None):
        '''
        Register a shortcut with calibre. calibre will manage the shortcut,
        automatically resolving conflicts and allowing the user to customize
        it.

        :param unique_name: A string that uniquely identifies this shortcut
        :param name: A user visible name describing the action performed by
        this shortcut
        :param default_keys: A tuple of keys that trigger this shortcut. Each
        key must be a string. For example: ('Ctrl+A', 'Alt+B', 'C',
        'Shift+Meta+D'). These keys will be assigned to the
        shortcut unless there is a conflict.
        :param action: A QAction object. The shortcut will cause this QAction
        to be triggered. Connect to its triggered signal in your code to
        respond to the shortcut.
        :param group: A string describing what "group" this shortcut belongs
        to. This is used to organize the list of shortcuts when the user is
        customizing them.
        '''
        if unique_name in self.shortcuts:
            name = self.shortcuts[unique_name]['name']
            raise NameConflict('Shortcut for %r already registered by %s'%(
                    unique_name, name))
        shortcut = {'name':name, 'desc':description, 'action': action,
                'default_keys':tuple(default_keys)}
        self.shortcuts[unique_name] = shortcut
        group = group if group else _('Miscellaneous')
        self.groups[group] = self.groups.get(group, []) + [unique_name]

    def unregister_shortcut(self, unique_name):
        '''
        Remove a registered shortcut. You need to call finalize() after you are
        done unregistering.
        '''
        self.shortcuts.pop(unique_name, None)
        for group in self.groups.itervalues():
            try:
                group.remove(unique_name)
            except ValueError:
                pass

    def finalize(self):
        custom_keys_map = {un:tuple(keys) for un, keys in self.config.get(
            'map', {}).iteritems()}
        self.keys_map = finalize(self.shortcuts, custom_keys_map=custom_keys_map)
        #import pprint
        # pprint.pprint(self.keys_map)

    def replace_action(self, unique_name, new_action):
        '''
        Replace the action associated with a shortcut.
        Once you're done calling replace_action() for all shortcuts you want
        replaced, call finalize() to have the shortcuts assigned to the replaced
        actions.
        '''
        sc = self.shortcuts[unique_name]
        sc['action'] = new_action
    def __init__(self, database, book_id, connections):
        self._connections = connections

        book_path = database.field_for('path', book_id).replace('/', os.sep)

        self._prefs = JSONConfig(os.path.join(book_path, 'book_settings'),
                                 base_path=LIBRARY)
        self._prefs.setdefault('asin', '')
        self._prefs.setdefault('goodreads_url', '')
        self._prefs.setdefault('aliases', {})
        self._prefs.setdefault('sample_xray', '')
        self._prefs.commit()

        self._title = database.field_for('title', book_id)
        self._author = ' & '.join(database.field_for('authors', book_id))

        self._asin = self._prefs['asin'] if self._prefs['asin'] != '' else None
        self._goodreads_url = self._prefs['goodreads_url']
        self._sample_xray = self._prefs['sample_xray']

        if not self._asin:
            identifiers = database.field_for('identifiers', book_id)
            if 'mobi-asin' in list(identifiers.keys()):
                self._asin = database.field_for(
                    'identifiers', book_id)['mobi-asin'].decode('ascii')
                self._prefs['asin'] = self._asin
            else:
                self._asin = self.search_for_asin_on_amazon(
                    self.title_and_author)
                if self._asin:
                    metadata = database.get_metadata(book_id)
                    identifiers = metadata.get_identifiers()
                    identifiers['mobi-asin'] = self._asin
                    metadata.set_identifiers(identifiers)
                    database.set_metadata(book_id, metadata)
                    self._prefs['asin'] = self._asin

        if self._goodreads_url == '':
            url = None
            if self._asin:
                url = self.search_for_goodreads_url(self._asin)
            if not url and self._title != 'Unknown' and self._author != 'Unknown':
                url = self.search_for_goodreads_url(self.title_and_author)

            if url:
                self._goodreads_url = url
                self._prefs['goodreads_url'] = self._goodreads_url
                if not self._asin:
                    self._asin = self.search_for_asin_on_goodreads(
                        self._goodreads_url)
                    if self._asin:
                        metadata = database.get_metadata(book_id)
                        identifiers = metadata.get_identifiers()
                        identifiers['mobi-asin'] = self._asin
                        metadata.set_identifiers(identifiers)
                        database.set_metadata(book_id, metadata)
                        self._prefs['asin'] = self._asin

        self._aliases = self._prefs['aliases']

        self.save()
Example #53
0
class Stores(OrderedDict):

    CHECK_INTERVAL = 24 * 60 * 60

    def builtins_loaded(self):
        self.last_check_time = 0
        self.version_map = {}
        self.cached_version_map = {}
        self.name_rmap = {}
        for key, val in self.iteritems():
            prefix, name = val.__module__.rpartition('.')[0::2]
            if prefix == 'calibre.gui2.store.stores' and name.endswith('_plugin'):
                module = sys.modules[val.__module__]
                sv = getattr(module, 'store_version', None)
                if sv is not None:
                    name = name.rpartition('_')[0]
                    self.version_map[name] = sv
                    self.name_rmap[name] = key
        self.cache_file = JSONConfig('store/plugin_cache')
        self.load_cache()

    def load_cache(self):
        # Load plugins from on disk cache
        remove = set()
        pat = re.compile(r'^store_version\s*=\s*(\d+)', re.M)
        for name, src in self.cache_file.iteritems():
            try:
                key = self.name_rmap[name]
            except KeyError:
                # Plugin has been disabled
                m = pat.search(src[:512])
                if m is not None:
                    try:
                        self.cached_version_map[name] = int(m.group(1))
                    except (TypeError, ValueError):
                        pass
                continue

            try:
                obj, ver = self.load_object(src, key)
            except VersionMismatch as e:
                self.cached_version_map[name] = e.ver
                continue
            except:
                import traceback
                prints('Failed to load cached store:', name)
                traceback.print_exc()
            else:
                if not self.replace_plugin(ver, name, obj, 'cached'):
                    # Builtin plugin is newer than cached
                    remove.add(name)

        if remove:
            with self.cache_file:
                for name in remove:
                    del self.cache_file[name]

    def check_for_updates(self):
        if hasattr(self, 'update_thread') and self.update_thread.is_alive():
            return
        if time.time() - self.last_check_time < self.CHECK_INTERVAL:
            return
        self.last_check_time = time.time()
        try:
            self.update_thread.start()
        except (RuntimeError, AttributeError):
            self.update_thread = Thread(target=self.do_update)
            self.update_thread.start()

    def join(self, timeout=None):
        hasattr(self, 'update_thread') and self.update_thread.join(timeout)

    def download_updates(self):
        ver_map = {name:max(ver, self.cached_version_map.get(name, -1))
            for name, ver in self.version_map.iteritems()}
        try:
            updates = download_updates(ver_map)
        except:
            import traceback
            traceback.print_exc()
        else:
            for name, code in updates:
                yield name, code

    def do_update(self):
        replacements = {}

        for name, src in self.download_updates():
            try:
                key = self.name_rmap[name]
            except KeyError:
                # Plugin has been disabled
                replacements[name] = src
                continue
            try:
                obj, ver = self.load_object(src, key)
            except VersionMismatch as e:
                self.cached_version_map[name] = e.ver
                replacements[name] = src
                continue
            except:
                import traceback
                prints('Failed to load downloaded store:', name)
                traceback.print_exc()
            else:
                if self.replace_plugin(ver, name, obj, 'downloaded'):
                    replacements[name] = src

        if replacements:
            with self.cache_file:
                for name, src in replacements.iteritems():
                    self.cache_file[name] = src

    def replace_plugin(self, ver, name, obj, source):
        if ver > self.version_map[name]:
            if DEBUG:
                prints('Loaded', source, 'store plugin for:',
                       self.name_rmap[name], 'at version:', ver)
            self[self.name_rmap[name]] = obj
            self.version_map[name] = ver
            return True
        return False

    def load_object(self, src, key):
        namespace = {}
        builtin = self[key]
        exec src in namespace
        ver = namespace['store_version']
        cls = None
        for x in namespace.itervalues():
            if (isinstance(x, type) and issubclass(x, StorePlugin) and x is not
                StorePlugin):
                cls = x
                break
        if cls is None:
            raise ValueError('No store plugin found')
        if cls.minimum_calibre_version > numeric_version:
            raise VersionMismatch(ver)
        return cls(builtin.gui, builtin.name, config=builtin.config,
                   base_plugin=builtin.base_plugin), ver
Example #54
0
 def prefs(self):
     if self._config_obj is None:
         from calibre.utils.config import JSONConfig
         self._config_obj = JSONConfig('metadata_sources/%s.json' %
                                       self.name)
     return self._config_obj
Example #55
0
    if data is None:
        icon = QIcon(I('blank.png'))
    else:
        pmap = QPixmap()
        pmap.loadFromData(bytes(data))
        icon = QIcon(pmap)
    return icon, entry.get('name', entry.get('Name')) or _('Unknown')

if iswindows:
    # Windows {{{
    from calibre.utils.winreg.default_programs import find_programs, friendly_app_name
    from calibre.utils.open_with.windows import load_icon_resource
    from win32process import CreateProcess, STARTUPINFO
    from win32event import WaitForInputIdle
    import win32con
    oprefs = JSONConfig('windows_open_with')

    def entry_sort_key(entry):
        return sort_key(entry.get('name') or '')

    def finalize_entry(entry):
        try:
            data = load_icon_resource(entry.pop('icon_resource', None), as_data=True)
        except Exception:
            data = None
            import traceback
            traceback.print_exc()
        if data:
            entry['icon_data'] = data
        return entry
Example #56
0
except ImportError as e:
    from PyQt4 import QtGui
    from PyQt4.Qt import QWidget, QGridLayout, QLabel, QLineEdit, QPushButton
from calibre.utils.config import JSONConfig

from calibre_plugins.extract_isbn.common_utils import KeyValueComboBox, KeyboardConfigDialog

STORE_NAME = 'Options'
KEY_PATH_BROWSER = 'pathBrowser'

DEFAULT_STORE_VALUES = {
    KEY_PATH_BROWSER: '',
}

# This is where all preferences for this plugin will be stored
plugin_prefs = JSONConfig('plugins/Bibi Calibre')

# Set defaults
plugin_prefs.defaults[STORE_NAME] = DEFAULT_STORE_VALUES

class ConfigWidget(QWidget):

    def __init__(self, plugin_action):
        QWidget.__init__(self)
        self.plugin_action = plugin_action
        layout = QGridLayout(self)
        self.setLayout(layout)

        c = plugin_prefs[STORE_NAME]

        layout.addWidget(QLabel('Browser command:', self), 0, 0)
Example #57
0
import bz2
import hashlib
import json
import sys
import time
from threading import Thread

import calibre.ebooks.metadata.sources.search_engines as builtin_search_engines
from calibre import as_unicode, prints
from calibre.constants import DEBUG, numeric_version
from calibre.ebooks.metadata.sources.base import Source
from calibre.utils.config import JSONConfig
from calibre.utils.https import get_https_resource_securely

cache = JSONConfig('metadata-sources-cache.json')

UPDATE_INTERVAL = 12 * 60 * 60

current_search_engines = builtin_search_engines


def search_engines_module():
    return current_search_engines


def debug_print(*args, **k):
    if DEBUG:
        prints(*args, **k)

Example #58
0
__license__   = 'GPL v3'
__copyright__ = '2013, Marcell Mars <*****@*****.**>'
__docformat__ = 'restructuredtext en'

from PyQt4.Qt import QWidget, QHBoxLayout, QLabel, QLineEdit, QVBoxLayout

from calibre.utils.config import JSONConfig

# This is where all preferences for this plugin will be stored
# Remember that this name (i.e. plugins/interface_demo) is also
# in a global namespace, so make it as unique as possible.
# You should always prefix your config file name with plugins/,
# so as to ensure you dont accidentally clobber a calibre config file

prefs = JSONConfig('plugins/letssharebooks.conf')

# Set defaults
prefs.defaults['lsb_server'] = 'memoryoftheworld.org'
prefs.defaults['server_prefix'] = 'https'

try:
    prefs['library_uuid']
except:
    prefs.defaults['library_uuid'] = str(uuid.uuid4())

try:
    prefs.defaults['librarian']
except:
    prefs.defaults['librarian'] = 'l'