diff --git a/calibre-plugin/basicinihighlighter.py b/calibre-plugin/basicinihighlighter.py index b948a565..07f169b3 100644 --- a/calibre-plugin/basicinihighlighter.py +++ b/calibre-plugin/basicinihighlighter.py @@ -1,64 +1,64 @@ -# -*- coding: utf-8 -*- - -from __future__ import (unicode_literals, division, - print_function) - -__license__ = 'GPL v3' -__copyright__ = '2015, Jim Miller' -__docformat__ = 'restructuredtext en' - -import re - -try: - from PyQt5.Qt import (Qt, QSyntaxHighlighter, QTextCharFormat, QBrush) -except ImportError as e: - from PyQt4.Qt import (Qt, QSyntaxHighlighter, QTextCharFormat, QBrush) - -class BasicIniHighlighter(QSyntaxHighlighter): - ''' - QSyntaxHighlighter class for use with QTextEdit for highlighting - ini config files. - - I looked high and low to find a high lighter for basic ini config - format, so I'm leaving this in the project even though I'm not - using. - ''' - - def __init__( self, parent, theme ): - QSyntaxHighlighter.__init__( self, parent ) - self.parent = parent - - self.highlightingRules = [] - - # keyword - self.highlightingRules.append( HighlightingRule( r"^[^:=\s][^:=]*[:=]", - Qt.blue, - Qt.SolidPattern ) ) - - # section - self.highlightingRules.append( HighlightingRule( r"^\[[^\]]+\]", - Qt.darkBlue, - Qt.SolidPattern ) ) - - # comment - self.highlightingRules.append( HighlightingRule( r"#[^\n]*" , - Qt.darkYellow, - Qt.SolidPattern ) ) - - def highlightBlock( self, text ): - for rule in self.highlightingRules: - for match in rule.pattern.finditer(text): - self.setFormat( match.start(), match.end()-match.start(), rule.highlight ) - self.setCurrentBlockState( 0 ) - -class HighlightingRule(): - def __init__( self, pattern, color, style ): - if isinstance(pattern,basestring): - self.pattern = re.compile(pattern) - else: - self.pattern=pattern - charfmt = QTextCharFormat() - brush = QBrush(color, style) - charfmt.setForeground(brush) - self.highlight = charfmt - +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2015, Jim Miller' +__docformat__ = 'restructuredtext en' + +import re + +try: + from PyQt5.Qt import (Qt, QSyntaxHighlighter, QTextCharFormat, QBrush) +except ImportError as e: + from PyQt4.Qt import (Qt, QSyntaxHighlighter, QTextCharFormat, QBrush) + +class BasicIniHighlighter(QSyntaxHighlighter): + ''' + QSyntaxHighlighter class for use with QTextEdit for highlighting + ini config files. + + I looked high and low to find a high lighter for basic ini config + format, so I'm leaving this in the project even though I'm not + using. + ''' + + def __init__( self, parent, theme ): + QSyntaxHighlighter.__init__( self, parent ) + self.parent = parent + + self.highlightingRules = [] + + # keyword + self.highlightingRules.append( HighlightingRule( r"^[^:=\s][^:=]*[:=]", + Qt.blue, + Qt.SolidPattern ) ) + + # section + self.highlightingRules.append( HighlightingRule( r"^\[[^\]]+\]", + Qt.darkBlue, + Qt.SolidPattern ) ) + + # comment + self.highlightingRules.append( HighlightingRule( r"#[^\n]*" , + Qt.darkYellow, + Qt.SolidPattern ) ) + + def highlightBlock( self, text ): + for rule in self.highlightingRules: + for match in rule.pattern.finditer(text): + self.setFormat( match.start(), match.end()-match.start(), rule.highlight ) + self.setCurrentBlockState( 0 ) + +class HighlightingRule(): + def __init__( self, pattern, color, style ): + if isinstance(pattern,basestring): + self.pattern = re.compile(pattern) + else: + self.pattern=pattern + charfmt = QTextCharFormat() + brush = QBrush(color, style) + charfmt.setForeground(brush) + self.highlight = charfmt + diff --git a/calibre-plugin/common_utils.py b/calibre-plugin/common_utils.py index d032027e..62648ee9 100644 --- a/calibre-plugin/common_utils.py +++ b/calibre-plugin/common_utils.py @@ -1,552 +1,552 @@ -# -*- coding: utf-8 -*- - -from __future__ import (unicode_literals, division, absolute_import, - print_function) - -__license__ = 'GPL v3' -__copyright__ = '2011, Grant Drake , 2015, Jim Miller' -__docformat__ = 'restructuredtext en' - -import os -try: - from PyQt5 import QtWidgets as QtGui - from PyQt5.Qt import (Qt, QIcon, QPixmap, QLabel, QDialog, QHBoxLayout, - QTableWidgetItem, QFont, QLineEdit, QComboBox, - QVBoxLayout, QDialogButtonBox, QStyledItemDelegate, QDateTime, - QTextEdit, QListWidget, QAbstractItemView) -except ImportError as e: - from PyQt4 import QtGui - from PyQt4.Qt import (Qt, QIcon, QPixmap, QLabel, QDialog, QHBoxLayout, - QTableWidgetItem, QFont, QLineEdit, QComboBox, - QVBoxLayout, QDialogButtonBox, QStyledItemDelegate, QDateTime, - QTextEdit, QListWidget, QAbstractItemView) - -from calibre.constants import iswindows -from calibre.gui2 import gprefs, error_dialog, UNDEFINED_QDATETIME, info_dialog -from calibre.gui2.actions import menu_action_unique_name -from calibre.gui2.keyboard import ShortcutConfig -from calibre.utils.config import config_dir -from calibre.utils.date import now, format_date, qt_to_dt, UNDEFINED_DATE - -# Global definition of our plugin name. Used for common functions that require this. -plugin_name = None -# Global definition of our plugin resources. Used to share between the xxxAction and xxxBase -# classes if you need any zip images to be displayed on the configuration dialog. -plugin_icon_resources = {} - - -def set_plugin_icon_resources(name, resources): - ''' - Set our global store of plugin name and icon resources for sharing between - the InterfaceAction class which reads them and the ConfigWidget - if needed for use on the customization dialog for this plugin. - ''' - global plugin_icon_resources, plugin_name - plugin_name = name - plugin_icon_resources = resources - - -def get_icon(icon_name): - ''' - Retrieve a QIcon for the named image from the zip file if it exists, - or if not then from Calibre's image cache. - ''' - if icon_name: - pixmap = get_pixmap(icon_name) - if pixmap is None: - # Look in Calibre's cache for the icon - return QIcon(I(icon_name)) - else: - return QIcon(pixmap) - return QIcon() - - -def get_pixmap(icon_name): - ''' - Retrieve a QPixmap for the named image - Any icons belonging to the plugin must be prefixed with 'images/' - ''' - global plugin_icon_resources, plugin_name - - if not icon_name.startswith('images/'): - # We know this is definitely not an icon belonging to this plugin - pixmap = QPixmap() - pixmap.load(I(icon_name)) - return pixmap - - # Check to see whether the icon exists as a Calibre resource - # This will enable skinning if the user stores icons within a folder like: - # ...\AppData\Roaming\calibre\resources\images\Plugin Name\ - if plugin_name: - local_images_dir = get_local_images_dir(plugin_name) - local_image_path = os.path.join(local_images_dir, icon_name.replace('images/', '')) - if os.path.exists(local_image_path): - pixmap = QPixmap() - pixmap.load(local_image_path) - return pixmap - - # As we did not find an icon elsewhere, look within our zip resources - if icon_name in plugin_icon_resources: - pixmap = QPixmap() - pixmap.loadFromData(plugin_icon_resources[icon_name]) - return pixmap - return None - - -def get_local_images_dir(subfolder=None): - ''' - Returns a path to the user's local resources/images folder - If a subfolder name parameter is specified, appends this to the path - ''' - images_dir = os.path.join(config_dir, 'resources/images') - if subfolder: - images_dir = os.path.join(images_dir, subfolder) - if iswindows: - images_dir = os.path.normpath(images_dir) - return images_dir - - -def create_menu_item(ia, parent_menu, menu_text, image=None, tooltip=None, - shortcut=(), triggered=None, is_checked=None): - ''' - Create a menu action with the specified criteria and action - Note that if no shortcut is specified, will not appear in Preferences->Keyboard - This method should only be used for actions which either have no shortcuts, - or register their menus only once. Use create_menu_action_unique for all else. - ''' - if shortcut is not None: - if len(shortcut) == 0: - shortcut = () - else: - shortcut = shortcut - ac = ia.create_action(spec=(menu_text, None, tooltip, shortcut), - attr=menu_text) - if image: - ac.setIcon(get_icon(image)) - if triggered is not None: - ac.triggered.connect(triggered) - if is_checked is not None: - ac.setCheckable(True) - if is_checked: - ac.setChecked(True) - - parent_menu.addAction(ac) - return ac - - -def create_menu_action_unique(ia, parent_menu, menu_text, image=None, tooltip=None, - shortcut=None, triggered=None, is_checked=None, shortcut_name=None, - unique_name=None): - ''' - Create a menu action with the specified criteria and action, using the new - InterfaceAction.create_menu_action() function which ensures that regardless of - whether a shortcut is specified it will appear in Preferences->Keyboard - ''' - orig_shortcut = shortcut - kb = ia.gui.keyboard - if unique_name is None: - unique_name = menu_text - if not shortcut == False: - full_unique_name = menu_action_unique_name(ia, unique_name) - if full_unique_name in kb.shortcuts: - shortcut = False - else: - if shortcut is not None and not shortcut == False: - if len(shortcut) == 0: - shortcut = None - else: - shortcut = shortcut - - if shortcut_name is None: - shortcut_name = menu_text.replace('&','') - - ac = ia.create_menu_action(parent_menu, unique_name, menu_text, icon=None, shortcut=shortcut, - description=tooltip, triggered=triggered, shortcut_name=shortcut_name) - if shortcut == False and not orig_shortcut == False: - if ac.calibre_shortcut_unique_name in ia.gui.keyboard.shortcuts: - kb.replace_action(ac.calibre_shortcut_unique_name, ac) - if image: - ac.setIcon(get_icon(image)) - if is_checked is not None: - ac.setCheckable(True) - if is_checked: - ac.setChecked(True) - return ac - - -def swap_author_names(author): - if author.find(',') == -1: - return author - name_parts = author.strip().partition(',') - return name_parts[2].strip() + ' ' + name_parts[0] - - -def get_library_uuid(db): - try: - library_uuid = db.library_id - except: - library_uuid = '' - return library_uuid - - -class ImageLabel(QLabel): - - def __init__(self, parent, icon_name, size=16): - QLabel.__init__(self, parent) - pixmap = get_pixmap(icon_name) - self.setPixmap(pixmap) - self.setMaximumSize(size, size) - self.setScaledContents(True) - - -class ImageTitleLayout(QHBoxLayout): - ''' - A reusable layout widget displaying an image followed by a title - ''' - def __init__(self, parent, icon_name, title, tooltip=None): - QHBoxLayout.__init__(self) - title_image_label = QLabel(parent) - pixmap = get_pixmap(icon_name) - if pixmap is None: - pixmap = get_pixmap('library.png') - # error_dialog(parent, _('Restart required'), - # _('You must restart Calibre before using this plugin!'), show=True) - else: - title_image_label.setPixmap(pixmap) - title_image_label.setMaximumSize(32, 32) - title_image_label.setScaledContents(True) - self.addWidget(title_image_label) - - title_font = QFont() - title_font.setPointSize(16) - shelf_label = QLabel(title, parent) - shelf_label.setFont(title_font) - self.addWidget(shelf_label) - self.insertStretch(-1) - - if tooltip: - title_image_label.setToolTip(tooltip) - shelf_label.setToolTip(tooltip) - -class SizePersistedDialog(QDialog): - ''' - This dialog is a base class for any dialogs that want their size/position - restored when they are next opened. - ''' - def __init__(self, parent, unique_pref_name): - QDialog.__init__(self, parent) - self.unique_pref_name = unique_pref_name - self.geom = gprefs.get(unique_pref_name, None) - self.finished.connect(self.dialog_closing) - - def resize_dialog(self): - if self.geom is None: - self.resize(self.sizeHint()) - else: - self.restoreGeometry(self.geom) - - def dialog_closing(self, result): - self.geom = bytearray(self.saveGeometry()) - gprefs[self.unique_pref_name] = self.geom - - -class ReadOnlyTableWidgetItem(QTableWidgetItem): - - def __init__(self, text): - if text is None: - text = '' - QTableWidgetItem.__init__(self, text, QtGui.QTableWidgetItem.UserType) - self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled) - - -class RatingTableWidgetItem(QTableWidgetItem): - - def __init__(self, rating, is_read_only=False): - QTableWidgetItem.__init__(self, '', QtGui.QTableWidgetItem.UserType) - self.setData(Qt.DisplayRole, rating) - if is_read_only: - self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled) - - -class DateTableWidgetItem(QTableWidgetItem): - - def __init__(self, date_read, is_read_only=False, default_to_today=False): - if date_read == UNDEFINED_DATE and default_to_today: - date_read = now() - if is_read_only: - QTableWidgetItem.__init__(self, format_date(date_read, None), QtGui.QTableWidgetItem.UserType) - self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled) - else: - QTableWidgetItem.__init__(self, '', QtGui.QTableWidgetItem.UserType) - self.setData(Qt.DisplayRole, QDateTime(date_read)) - - -class NoWheelComboBox(QComboBox): - - def wheelEvent (self, event): - # Disable the mouse wheel on top of the combo box changing selection as plays havoc in a grid - event.ignore() - - -class CheckableTableWidgetItem(QTableWidgetItem): - - def __init__(self, checked=False, is_tristate=False): - QTableWidgetItem.__init__(self, '') - self.setFlags(Qt.ItemFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled )) - if is_tristate: - self.setFlags(self.flags() | Qt.ItemIsTristate) - if checked: - self.setCheckState(Qt.Checked) - else: - if is_tristate and checked is None: - self.setCheckState(Qt.PartiallyChecked) - else: - self.setCheckState(Qt.Unchecked) - - def get_boolean_value(self): - ''' - Return a boolean value indicating whether checkbox is checked - If this is a tristate checkbox, a partially checked value is returned as None - ''' - if self.checkState() == Qt.PartiallyChecked: - return None - else: - return self.checkState() == Qt.Checked - - -class TextIconWidgetItem(QTableWidgetItem): - - def __init__(self, text, icon): - QTableWidgetItem.__init__(self, text) - if icon: - self.setIcon(icon) - - -class ReadOnlyTextIconWidgetItem(ReadOnlyTableWidgetItem): - - def __init__(self, text, icon): - ReadOnlyTableWidgetItem.__init__(self, text) - if icon: - self.setIcon(icon) - - -class ReadOnlyLineEdit(QLineEdit): - - def __init__(self, text, parent): - if text is None: - text = '' - QLineEdit.__init__(self, text, parent) - self.setEnabled(False) - - -class KeyValueComboBox(QComboBox): - - def __init__(self, parent, values, selected_key): - QComboBox.__init__(self, parent) - self.values = values - self.populate_combo(selected_key) - - def populate_combo(self, selected_key): - self.clear() - selected_idx = idx = -1 - for key, value in self.values.iteritems(): - idx = idx + 1 - self.addItem(value) - if key == selected_key: - selected_idx = idx - self.setCurrentIndex(selected_idx) - - def selected_key(self): - for key, value in self.values.iteritems(): - if value == unicode(self.currentText()).strip(): - return key - - -class CustomColumnComboBox(QComboBox): - - def __init__(self, parent, custom_columns, selected_column, initial_items=['']): - QComboBox.__init__(self, parent) - self.populate_combo(custom_columns, selected_column, initial_items) - - def populate_combo(self, custom_columns, selected_column, initial_items=['']): - self.clear() - self.column_names = initial_items - if len(initial_items) > 0: - self.addItems(initial_items) - selected_idx = 0 - for idx, value in enumerate(initial_items): - if value == selected_column: - selected_idx = idx - for key in sorted(custom_columns.keys()): - self.column_names.append(key) - self.addItem('%s (%s)'%(key, custom_columns[key]['name'])) - if key == selected_column: - selected_idx = len(self.column_names) - 1 - self.setCurrentIndex(selected_idx) - - def get_selected_column(self): - return self.column_names[self.currentIndex()] - - -class KeyboardConfigDialog(SizePersistedDialog): - ''' - This dialog is used to allow editing of keyboard shortcuts. - ''' - def __init__(self, gui, group_name): - SizePersistedDialog.__init__(self, gui, 'FanFicFare plugin:Keyboard shortcut dialog') - self.gui = gui - self.setWindowTitle(_('Keyboard shortcuts')) - layout = QVBoxLayout(self) - self.setLayout(layout) - - self.keyboard_widget = ShortcutConfig(self) - layout.addWidget(self.keyboard_widget) - self.group_name = group_name - - button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) - button_box.accepted.connect(self.commit) - button_box.rejected.connect(self.reject) - layout.addWidget(button_box) - - # Cause our dialog size to be restored from prefs or created on first usage - self.resize_dialog() - self.initialize() - - def initialize(self): - self.keyboard_widget.initialize(self.gui.keyboard) - self.keyboard_widget.highlight_group(self.group_name) - - def commit(self): - self.keyboard_widget.commit() - self.accept() - - -class DateDelegate(QStyledItemDelegate): - ''' - Delegate for dates. Because this delegate stores the - format as an instance variable, a new instance must be created for each - column. This differs from all the other delegates. - ''' - def __init__(self, parent): - QStyledItemDelegate.__init__(self, parent) - self.format = 'dd MMM yyyy' - - def displayText(self, val, locale): - d = val.toDateTime() - if d <= UNDEFINED_QDATETIME: - return '' - return format_date(qt_to_dt(d, as_utc=False), self.format) - - def createEditor(self, parent, option, index): - qde = QStyledItemDelegate.createEditor(self, parent, option, index) - qde.setDisplayFormat(self.format) - qde.setMinimumDateTime(UNDEFINED_QDATETIME) - qde.setSpecialValueText(_('Undefined')) - qde.setCalendarPopup(True) - return qde - - def setEditorData(self, editor, index): - val = index.model().data(index, Qt.DisplayRole).toDateTime() - if val is None or val == UNDEFINED_QDATETIME: - val = now() - editor.setDateTime(val) - - def setModelData(self, editor, model, index): - val = editor.dateTime() - if val <= UNDEFINED_QDATETIME: - model.setData(index, UNDEFINED_QDATETIME, Qt.EditRole) - else: - model.setData(index, QDateTime(val), Qt.EditRole) - -class PrefsViewerDialog(SizePersistedDialog): - - def __init__(self, gui, namespace): - SizePersistedDialog.__init__(self, gui, _('Prefs Viewer dialog')) - self.setWindowTitle(_('Preferences for: ')+namespace) - - self.gui = gui - self.db = gui.current_db - self.namespace = namespace - self._init_controls() - self.resize_dialog() - - self._populate_settings() - - if self.keys_list.count(): - self.keys_list.setCurrentRow(0) - - def _init_controls(self): - layout = QVBoxLayout(self) - self.setLayout(layout) - - ml = QHBoxLayout() - layout.addLayout(ml, 1) - - self.keys_list = QListWidget(self) - self.keys_list.setSelectionMode(QAbstractItemView.SingleSelection) - self.keys_list.setFixedWidth(150) - self.keys_list.setAlternatingRowColors(True) - ml.addWidget(self.keys_list) - self.value_text = QTextEdit(self) - self.value_text.setTabStopWidth(24) - self.value_text.setReadOnly(True) - ml.addWidget(self.value_text, 1) - - button_box = QDialogButtonBox(QDialogButtonBox.Ok) - button_box.accepted.connect(self.accept) - self.clear_button = button_box.addButton(_('Clear'), QDialogButtonBox.ResetRole) - self.clear_button.setIcon(get_icon('trash.png')) - self.clear_button.setToolTip(_('Clear all settings for this plugin')) - self.clear_button.clicked.connect(self._clear_settings) - layout.addWidget(button_box) - - def _populate_settings(self): - self.keys_list.clear() - ns_prefix = self._get_ns_prefix() - keys = sorted([k[len(ns_prefix):] for k in self.db.prefs.iterkeys() - if k.startswith(ns_prefix)]) - for key in keys: - self.keys_list.addItem(key) - self.keys_list.setMinimumWidth(self.keys_list.sizeHintForColumn(0)) - self.keys_list.currentRowChanged[int].connect(self._current_row_changed) - - def _current_row_changed(self, new_row): - if new_row < 0: - self.value_text.clear() - return - key = unicode(self.keys_list.currentItem().text()) - val = self.db.prefs.get_namespaced(self.namespace, key, '') - self.value_text.setPlainText(self.db.prefs.to_raw(val)) - - def _get_ns_prefix(self): - return 'namespaced:%s:'% self.namespace - - def _clear_settings(self): - from calibre.gui2.dialogs.confirm_delete import confirm - message = '

' + _('Are you sure you want to clear your settings in this library for this plugin?') + '

' \ - + '

' + _('Any settings in other libraries or stored in a JSON file in your calibre plugins folder will not be touched.') + '

' \ - + '

' + _('You must restart calibre afterwards.') + '

' - if not confirm(message, self.namespace+'_clear_settings', self): - return - ns_prefix = self._get_ns_prefix() - keys = [k for k in self.db.prefs.iterkeys() if k.startswith(ns_prefix)] - for k in keys: - del self.db.prefs[k] - self._populate_settings() - d = info_dialog(self, 'Settings deleted', - '

' + _('All settings for this plugin in this library have been cleared.') + '

' \ - + '

' + _('Please restart calibre now.') + '

', - show_copy_button=False) - b = d.bb.addButton(_('Restart calibre now'), d.bb.AcceptRole) - b.setIcon(QIcon(I('lt.png'))) - d.do_restart = False - def rf(): - d.do_restart = True - b.clicked.connect(rf) - d.set_details('') - d.exec_() - b.clicked.disconnect() - self.close() - if d.do_restart: - self.gui.quit(restart=True) - +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2011, Grant Drake , 2015, Jim Miller' +__docformat__ = 'restructuredtext en' + +import os +try: + from PyQt5 import QtWidgets as QtGui + from PyQt5.Qt import (Qt, QIcon, QPixmap, QLabel, QDialog, QHBoxLayout, + QTableWidgetItem, QFont, QLineEdit, QComboBox, + QVBoxLayout, QDialogButtonBox, QStyledItemDelegate, QDateTime, + QTextEdit, QListWidget, QAbstractItemView) +except ImportError as e: + from PyQt4 import QtGui + from PyQt4.Qt import (Qt, QIcon, QPixmap, QLabel, QDialog, QHBoxLayout, + QTableWidgetItem, QFont, QLineEdit, QComboBox, + QVBoxLayout, QDialogButtonBox, QStyledItemDelegate, QDateTime, + QTextEdit, QListWidget, QAbstractItemView) + +from calibre.constants import iswindows +from calibre.gui2 import gprefs, error_dialog, UNDEFINED_QDATETIME, info_dialog +from calibre.gui2.actions import menu_action_unique_name +from calibre.gui2.keyboard import ShortcutConfig +from calibre.utils.config import config_dir +from calibre.utils.date import now, format_date, qt_to_dt, UNDEFINED_DATE + +# Global definition of our plugin name. Used for common functions that require this. +plugin_name = None +# Global definition of our plugin resources. Used to share between the xxxAction and xxxBase +# classes if you need any zip images to be displayed on the configuration dialog. +plugin_icon_resources = {} + + +def set_plugin_icon_resources(name, resources): + ''' + Set our global store of plugin name and icon resources for sharing between + the InterfaceAction class which reads them and the ConfigWidget + if needed for use on the customization dialog for this plugin. + ''' + global plugin_icon_resources, plugin_name + plugin_name = name + plugin_icon_resources = resources + + +def get_icon(icon_name): + ''' + Retrieve a QIcon for the named image from the zip file if it exists, + or if not then from Calibre's image cache. + ''' + if icon_name: + pixmap = get_pixmap(icon_name) + if pixmap is None: + # Look in Calibre's cache for the icon + return QIcon(I(icon_name)) + else: + return QIcon(pixmap) + return QIcon() + + +def get_pixmap(icon_name): + ''' + Retrieve a QPixmap for the named image + Any icons belonging to the plugin must be prefixed with 'images/' + ''' + global plugin_icon_resources, plugin_name + + if not icon_name.startswith('images/'): + # We know this is definitely not an icon belonging to this plugin + pixmap = QPixmap() + pixmap.load(I(icon_name)) + return pixmap + + # Check to see whether the icon exists as a Calibre resource + # This will enable skinning if the user stores icons within a folder like: + # ...\AppData\Roaming\calibre\resources\images\Plugin Name\ + if plugin_name: + local_images_dir = get_local_images_dir(plugin_name) + local_image_path = os.path.join(local_images_dir, icon_name.replace('images/', '')) + if os.path.exists(local_image_path): + pixmap = QPixmap() + pixmap.load(local_image_path) + return pixmap + + # As we did not find an icon elsewhere, look within our zip resources + if icon_name in plugin_icon_resources: + pixmap = QPixmap() + pixmap.loadFromData(plugin_icon_resources[icon_name]) + return pixmap + return None + + +def get_local_images_dir(subfolder=None): + ''' + Returns a path to the user's local resources/images folder + If a subfolder name parameter is specified, appends this to the path + ''' + images_dir = os.path.join(config_dir, 'resources/images') + if subfolder: + images_dir = os.path.join(images_dir, subfolder) + if iswindows: + images_dir = os.path.normpath(images_dir) + return images_dir + + +def create_menu_item(ia, parent_menu, menu_text, image=None, tooltip=None, + shortcut=(), triggered=None, is_checked=None): + ''' + Create a menu action with the specified criteria and action + Note that if no shortcut is specified, will not appear in Preferences->Keyboard + This method should only be used for actions which either have no shortcuts, + or register their menus only once. Use create_menu_action_unique for all else. + ''' + if shortcut is not None: + if len(shortcut) == 0: + shortcut = () + else: + shortcut = shortcut + ac = ia.create_action(spec=(menu_text, None, tooltip, shortcut), + attr=menu_text) + if image: + ac.setIcon(get_icon(image)) + if triggered is not None: + ac.triggered.connect(triggered) + if is_checked is not None: + ac.setCheckable(True) + if is_checked: + ac.setChecked(True) + + parent_menu.addAction(ac) + return ac + + +def create_menu_action_unique(ia, parent_menu, menu_text, image=None, tooltip=None, + shortcut=None, triggered=None, is_checked=None, shortcut_name=None, + unique_name=None): + ''' + Create a menu action with the specified criteria and action, using the new + InterfaceAction.create_menu_action() function which ensures that regardless of + whether a shortcut is specified it will appear in Preferences->Keyboard + ''' + orig_shortcut = shortcut + kb = ia.gui.keyboard + if unique_name is None: + unique_name = menu_text + if not shortcut == False: + full_unique_name = menu_action_unique_name(ia, unique_name) + if full_unique_name in kb.shortcuts: + shortcut = False + else: + if shortcut is not None and not shortcut == False: + if len(shortcut) == 0: + shortcut = None + else: + shortcut = shortcut + + if shortcut_name is None: + shortcut_name = menu_text.replace('&','') + + ac = ia.create_menu_action(parent_menu, unique_name, menu_text, icon=None, shortcut=shortcut, + description=tooltip, triggered=triggered, shortcut_name=shortcut_name) + if shortcut == False and not orig_shortcut == False: + if ac.calibre_shortcut_unique_name in ia.gui.keyboard.shortcuts: + kb.replace_action(ac.calibre_shortcut_unique_name, ac) + if image: + ac.setIcon(get_icon(image)) + if is_checked is not None: + ac.setCheckable(True) + if is_checked: + ac.setChecked(True) + return ac + + +def swap_author_names(author): + if author.find(',') == -1: + return author + name_parts = author.strip().partition(',') + return name_parts[2].strip() + ' ' + name_parts[0] + + +def get_library_uuid(db): + try: + library_uuid = db.library_id + except: + library_uuid = '' + return library_uuid + + +class ImageLabel(QLabel): + + def __init__(self, parent, icon_name, size=16): + QLabel.__init__(self, parent) + pixmap = get_pixmap(icon_name) + self.setPixmap(pixmap) + self.setMaximumSize(size, size) + self.setScaledContents(True) + + +class ImageTitleLayout(QHBoxLayout): + ''' + A reusable layout widget displaying an image followed by a title + ''' + def __init__(self, parent, icon_name, title, tooltip=None): + QHBoxLayout.__init__(self) + title_image_label = QLabel(parent) + pixmap = get_pixmap(icon_name) + if pixmap is None: + pixmap = get_pixmap('library.png') + # error_dialog(parent, _('Restart required'), + # _('You must restart Calibre before using this plugin!'), show=True) + else: + title_image_label.setPixmap(pixmap) + title_image_label.setMaximumSize(32, 32) + title_image_label.setScaledContents(True) + self.addWidget(title_image_label) + + title_font = QFont() + title_font.setPointSize(16) + shelf_label = QLabel(title, parent) + shelf_label.setFont(title_font) + self.addWidget(shelf_label) + self.insertStretch(-1) + + if tooltip: + title_image_label.setToolTip(tooltip) + shelf_label.setToolTip(tooltip) + +class SizePersistedDialog(QDialog): + ''' + This dialog is a base class for any dialogs that want their size/position + restored when they are next opened. + ''' + def __init__(self, parent, unique_pref_name): + QDialog.__init__(self, parent) + self.unique_pref_name = unique_pref_name + self.geom = gprefs.get(unique_pref_name, None) + self.finished.connect(self.dialog_closing) + + def resize_dialog(self): + if self.geom is None: + self.resize(self.sizeHint()) + else: + self.restoreGeometry(self.geom) + + def dialog_closing(self, result): + self.geom = bytearray(self.saveGeometry()) + gprefs[self.unique_pref_name] = self.geom + + +class ReadOnlyTableWidgetItem(QTableWidgetItem): + + def __init__(self, text): + if text is None: + text = '' + QTableWidgetItem.__init__(self, text, QtGui.QTableWidgetItem.UserType) + self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled) + + +class RatingTableWidgetItem(QTableWidgetItem): + + def __init__(self, rating, is_read_only=False): + QTableWidgetItem.__init__(self, '', QtGui.QTableWidgetItem.UserType) + self.setData(Qt.DisplayRole, rating) + if is_read_only: + self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled) + + +class DateTableWidgetItem(QTableWidgetItem): + + def __init__(self, date_read, is_read_only=False, default_to_today=False): + if date_read == UNDEFINED_DATE and default_to_today: + date_read = now() + if is_read_only: + QTableWidgetItem.__init__(self, format_date(date_read, None), QtGui.QTableWidgetItem.UserType) + self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled) + else: + QTableWidgetItem.__init__(self, '', QtGui.QTableWidgetItem.UserType) + self.setData(Qt.DisplayRole, QDateTime(date_read)) + + +class NoWheelComboBox(QComboBox): + + def wheelEvent (self, event): + # Disable the mouse wheel on top of the combo box changing selection as plays havoc in a grid + event.ignore() + + +class CheckableTableWidgetItem(QTableWidgetItem): + + def __init__(self, checked=False, is_tristate=False): + QTableWidgetItem.__init__(self, '') + self.setFlags(Qt.ItemFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled )) + if is_tristate: + self.setFlags(self.flags() | Qt.ItemIsTristate) + if checked: + self.setCheckState(Qt.Checked) + else: + if is_tristate and checked is None: + self.setCheckState(Qt.PartiallyChecked) + else: + self.setCheckState(Qt.Unchecked) + + def get_boolean_value(self): + ''' + Return a boolean value indicating whether checkbox is checked + If this is a tristate checkbox, a partially checked value is returned as None + ''' + if self.checkState() == Qt.PartiallyChecked: + return None + else: + return self.checkState() == Qt.Checked + + +class TextIconWidgetItem(QTableWidgetItem): + + def __init__(self, text, icon): + QTableWidgetItem.__init__(self, text) + if icon: + self.setIcon(icon) + + +class ReadOnlyTextIconWidgetItem(ReadOnlyTableWidgetItem): + + def __init__(self, text, icon): + ReadOnlyTableWidgetItem.__init__(self, text) + if icon: + self.setIcon(icon) + + +class ReadOnlyLineEdit(QLineEdit): + + def __init__(self, text, parent): + if text is None: + text = '' + QLineEdit.__init__(self, text, parent) + self.setEnabled(False) + + +class KeyValueComboBox(QComboBox): + + def __init__(self, parent, values, selected_key): + QComboBox.__init__(self, parent) + self.values = values + self.populate_combo(selected_key) + + def populate_combo(self, selected_key): + self.clear() + selected_idx = idx = -1 + for key, value in self.values.iteritems(): + idx = idx + 1 + self.addItem(value) + if key == selected_key: + selected_idx = idx + self.setCurrentIndex(selected_idx) + + def selected_key(self): + for key, value in self.values.iteritems(): + if value == unicode(self.currentText()).strip(): + return key + + +class CustomColumnComboBox(QComboBox): + + def __init__(self, parent, custom_columns, selected_column, initial_items=['']): + QComboBox.__init__(self, parent) + self.populate_combo(custom_columns, selected_column, initial_items) + + def populate_combo(self, custom_columns, selected_column, initial_items=['']): + self.clear() + self.column_names = initial_items + if len(initial_items) > 0: + self.addItems(initial_items) + selected_idx = 0 + for idx, value in enumerate(initial_items): + if value == selected_column: + selected_idx = idx + for key in sorted(custom_columns.keys()): + self.column_names.append(key) + self.addItem('%s (%s)'%(key, custom_columns[key]['name'])) + if key == selected_column: + selected_idx = len(self.column_names) - 1 + self.setCurrentIndex(selected_idx) + + def get_selected_column(self): + return self.column_names[self.currentIndex()] + + +class KeyboardConfigDialog(SizePersistedDialog): + ''' + This dialog is used to allow editing of keyboard shortcuts. + ''' + def __init__(self, gui, group_name): + SizePersistedDialog.__init__(self, gui, 'FanFicFare plugin:Keyboard shortcut dialog') + self.gui = gui + self.setWindowTitle(_('Keyboard shortcuts')) + layout = QVBoxLayout(self) + self.setLayout(layout) + + self.keyboard_widget = ShortcutConfig(self) + layout.addWidget(self.keyboard_widget) + self.group_name = group_name + + button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + button_box.accepted.connect(self.commit) + button_box.rejected.connect(self.reject) + layout.addWidget(button_box) + + # Cause our dialog size to be restored from prefs or created on first usage + self.resize_dialog() + self.initialize() + + def initialize(self): + self.keyboard_widget.initialize(self.gui.keyboard) + self.keyboard_widget.highlight_group(self.group_name) + + def commit(self): + self.keyboard_widget.commit() + self.accept() + + +class DateDelegate(QStyledItemDelegate): + ''' + Delegate for dates. Because this delegate stores the + format as an instance variable, a new instance must be created for each + column. This differs from all the other delegates. + ''' + def __init__(self, parent): + QStyledItemDelegate.__init__(self, parent) + self.format = 'dd MMM yyyy' + + def displayText(self, val, locale): + d = val.toDateTime() + if d <= UNDEFINED_QDATETIME: + return '' + return format_date(qt_to_dt(d, as_utc=False), self.format) + + def createEditor(self, parent, option, index): + qde = QStyledItemDelegate.createEditor(self, parent, option, index) + qde.setDisplayFormat(self.format) + qde.setMinimumDateTime(UNDEFINED_QDATETIME) + qde.setSpecialValueText(_('Undefined')) + qde.setCalendarPopup(True) + return qde + + def setEditorData(self, editor, index): + val = index.model().data(index, Qt.DisplayRole).toDateTime() + if val is None or val == UNDEFINED_QDATETIME: + val = now() + editor.setDateTime(val) + + def setModelData(self, editor, model, index): + val = editor.dateTime() + if val <= UNDEFINED_QDATETIME: + model.setData(index, UNDEFINED_QDATETIME, Qt.EditRole) + else: + model.setData(index, QDateTime(val), Qt.EditRole) + +class PrefsViewerDialog(SizePersistedDialog): + + def __init__(self, gui, namespace): + SizePersistedDialog.__init__(self, gui, _('Prefs Viewer dialog')) + self.setWindowTitle(_('Preferences for: ')+namespace) + + self.gui = gui + self.db = gui.current_db + self.namespace = namespace + self._init_controls() + self.resize_dialog() + + self._populate_settings() + + if self.keys_list.count(): + self.keys_list.setCurrentRow(0) + + def _init_controls(self): + layout = QVBoxLayout(self) + self.setLayout(layout) + + ml = QHBoxLayout() + layout.addLayout(ml, 1) + + self.keys_list = QListWidget(self) + self.keys_list.setSelectionMode(QAbstractItemView.SingleSelection) + self.keys_list.setFixedWidth(150) + self.keys_list.setAlternatingRowColors(True) + ml.addWidget(self.keys_list) + self.value_text = QTextEdit(self) + self.value_text.setTabStopWidth(24) + self.value_text.setReadOnly(True) + ml.addWidget(self.value_text, 1) + + button_box = QDialogButtonBox(QDialogButtonBox.Ok) + button_box.accepted.connect(self.accept) + self.clear_button = button_box.addButton(_('Clear'), QDialogButtonBox.ResetRole) + self.clear_button.setIcon(get_icon('trash.png')) + self.clear_button.setToolTip(_('Clear all settings for this plugin')) + self.clear_button.clicked.connect(self._clear_settings) + layout.addWidget(button_box) + + def _populate_settings(self): + self.keys_list.clear() + ns_prefix = self._get_ns_prefix() + keys = sorted([k[len(ns_prefix):] for k in self.db.prefs.iterkeys() + if k.startswith(ns_prefix)]) + for key in keys: + self.keys_list.addItem(key) + self.keys_list.setMinimumWidth(self.keys_list.sizeHintForColumn(0)) + self.keys_list.currentRowChanged[int].connect(self._current_row_changed) + + def _current_row_changed(self, new_row): + if new_row < 0: + self.value_text.clear() + return + key = unicode(self.keys_list.currentItem().text()) + val = self.db.prefs.get_namespaced(self.namespace, key, '') + self.value_text.setPlainText(self.db.prefs.to_raw(val)) + + def _get_ns_prefix(self): + return 'namespaced:%s:'% self.namespace + + def _clear_settings(self): + from calibre.gui2.dialogs.confirm_delete import confirm + message = '

' + _('Are you sure you want to clear your settings in this library for this plugin?') + '

' \ + + '

' + _('Any settings in other libraries or stored in a JSON file in your calibre plugins folder will not be touched.') + '

' \ + + '

' + _('You must restart calibre afterwards.') + '

' + if not confirm(message, self.namespace+'_clear_settings', self): + return + ns_prefix = self._get_ns_prefix() + keys = [k for k in self.db.prefs.iterkeys() if k.startswith(ns_prefix)] + for k in keys: + del self.db.prefs[k] + self._populate_settings() + d = info_dialog(self, 'Settings deleted', + '

' + _('All settings for this plugin in this library have been cleared.') + '

' \ + + '

' + _('Please restart calibre now.') + '

', + show_copy_button=False) + b = d.bb.addButton(_('Restart calibre now'), d.bb.AcceptRole) + b.setIcon(QIcon(I('lt.png'))) + d.do_restart = False + def rf(): + d.do_restart = True + b.clicked.connect(rf) + d.set_details('') + d.exec_() + b.clicked.disconnect() + self.close() + if d.do_restart: + self.gui.quit(restart=True) + diff --git a/calibre-plugin/fff_util.py b/calibre-plugin/fff_util.py index be351311..faf3f552 100644 --- a/calibre-plugin/fff_util.py +++ b/calibre-plugin/fff_util.py @@ -1,49 +1,49 @@ -# -*- coding: utf-8 -*- - -from __future__ import (unicode_literals, division, absolute_import, - print_function) - -__license__ = 'GPL v3' -__copyright__ = '2015, Jim Miller' -__docformat__ = 'restructuredtext en' - -from StringIO import StringIO -from ConfigParser import ParsingError - -import logging -logger = logging.getLogger(__name__) - -from calibre_plugins.fanficfare_plugin.fanficfare import adapters, exceptions -from calibre_plugins.fanficfare_plugin.fanficfare.configurable import Configuration -from calibre_plugins.fanficfare_plugin.prefs import prefs - -def get_fff_personalini(): - return prefs['personal.ini'] - -def get_fff_config(url,fileform="epub",personalini=None): - if not personalini: - personalini = get_fff_personalini() - sections=['unknown'] - try: - sections = adapters.getConfigSectionsFor(url) - except Exception as e: - logger.debug("Failed trying to get ini config for url(%s): %s, using section %s instead"%(url,e,sections)) - configuration = Configuration(sections,fileform) - configuration.readfp(StringIO(get_resources("plugin-defaults.ini"))) - configuration.readfp(StringIO(personalini)) - - return configuration - -def get_fff_adapter(url,fileform="epub",personalini=None): - return adapters.getAdapter(get_fff_config(url,fileform,personalini),url) - -def test_config(initext): - try: - configini = get_fff_config("test1.com?sid=555", - personalini=initext) - errors = configini.test_config() - except ParsingError as pe: - errors = pe.errors - - return errors - +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2015, Jim Miller' +__docformat__ = 'restructuredtext en' + +from StringIO import StringIO +from ConfigParser import ParsingError + +import logging +logger = logging.getLogger(__name__) + +from calibre_plugins.fanficfare_plugin.fanficfare import adapters, exceptions +from calibre_plugins.fanficfare_plugin.fanficfare.configurable import Configuration +from calibre_plugins.fanficfare_plugin.prefs import prefs + +def get_fff_personalini(): + return prefs['personal.ini'] + +def get_fff_config(url,fileform="epub",personalini=None): + if not personalini: + personalini = get_fff_personalini() + sections=['unknown'] + try: + sections = adapters.getConfigSectionsFor(url) + except Exception as e: + logger.debug("Failed trying to get ini config for url(%s): %s, using section %s instead"%(url,e,sections)) + configuration = Configuration(sections,fileform) + configuration.readfp(StringIO(get_resources("plugin-defaults.ini"))) + configuration.readfp(StringIO(personalini)) + + return configuration + +def get_fff_adapter(url,fileform="epub",personalini=None): + return adapters.getAdapter(get_fff_config(url,fileform,personalini),url) + +def test_config(initext): + try: + configini = get_fff_config("test1.com?sid=555", + personalini=initext) + errors = configini.test_config() + except ParsingError as pe: + errors = pe.errors + + return errors + diff --git a/calibre-plugin/inihighlighter.py b/calibre-plugin/inihighlighter.py index 5b169fcc..4c80ad48 100644 --- a/calibre-plugin/inihighlighter.py +++ b/calibre-plugin/inihighlighter.py @@ -1,120 +1,120 @@ -# -*- coding: utf-8 -*- - -from __future__ import (unicode_literals, division, - print_function) - -__license__ = 'GPL v3' -__copyright__ = '2017, Jim Miller' -__docformat__ = 'restructuredtext en' - -import re - -try: - from PyQt5.Qt import (Qt, QSyntaxHighlighter, QTextCharFormat, QBrush, QFont) -except ImportError as e: - from PyQt4.Qt import (Qt, QSyntaxHighlighter, QTextCharFormat, QBrush, QFont) - -class IniHighlighter(QSyntaxHighlighter): - ''' - QSyntaxHighlighter class for use with QTextEdit for highlighting - ini config files. - ''' - - def __init__( self, parent, sections=[], keywords=[], entries=[], entry_keywords=[] ): - QSyntaxHighlighter.__init__( self, parent ) - self.parent = parent - - self.highlightingRules = [] - - if entries: - # *known* entries - reentries = r'('+(r'|'.join(entries))+r')' - self.highlightingRules.append( HighlightingRule( r"\b"+reentries+r"\b", Qt.darkGreen ) ) - - # true/false -- just to be nice. - self.highlightingRules.append( HighlightingRule( r"\b(true|false)\b", Qt.darkGreen ) ) - - # *all* keywords -- change known later. - self.errorRule = HighlightingRule( r"^[^:=\s][^:=]*[:=]", Qt.red ) - self.highlightingRules.append( self.errorRule ) - - # *all* entry keywords -- change known later. - reentrykeywords = r'('+(r'|'.join([ e % r'[a-zA-Z0-9_]+' for e in entry_keywords ]))+r')' - self.highlightingRules.append( HighlightingRule( r"^(add_to_)?"+reentrykeywords+r"(_filelist)?\s*[:=]", Qt.darkMagenta ) ) - - if entries: # separate from known entries so entry named keyword won't be masked. - # *known* entry keywords - reentrykeywords = r'('+(r'|'.join([ e % reentries for e in entry_keywords ]))+r')' - self.highlightingRules.append( HighlightingRule( r"^(add_to_)?"+reentrykeywords+r"(_filelist)?\s*[:=]", Qt.blue ) ) - - # *known* keywords - rekeywords = r'('+(r'|'.join(keywords))+r')' - self.highlightingRules.append( HighlightingRule( r"^(add_to_)?"+rekeywords+r"(_filelist)?\s*[:=]", Qt.blue ) ) - - # *all* sections -- change known later. - self.highlightingRules.append( HighlightingRule( r"^\[[^\]]+\].*?$", Qt.red, QFont.Bold, blocknum=1 ) ) - - if sections: - # *known* sections - resections = r'('+(r'|'.join(sections))+r')' - resections = resections.replace('.','\.') #escape dots. - self.highlightingRules.append( HighlightingRule( r"^\["+resections+r"\]\s*$", Qt.darkBlue, QFont.Bold, blocknum=2 ) ) - - # test story sections - self.teststoryRule = HighlightingRule( r"^\[teststory:([0-9]+|defaults)\]", Qt.darkCyan, blocknum=3 ) - self.highlightingRules.append( self.teststoryRule ) - - # storyUrl sections - self.storyUrlRule = HighlightingRule( r"^\[https?://.*\]", Qt.darkMagenta, blocknum=4 ) - self.highlightingRules.append( self.storyUrlRule ) - - # NOT comments -- but can be custom columns, so don't flag. - #self.highlightingRules.append( HighlightingRule( r"(? 0: - blocknum = rule.blocknum - - if not is_comment: - # unknown section, error all: - if blocknum == 1 and blocknum == self.previousBlockState(): - self.setFormat( 0, len(text), self.errorRule.highlight ) - - # teststory section rules: - if blocknum == 3: - self.setFormat( 0, len(text), self.teststoryRule.highlight ) - - # storyUrl section rules: - if blocknum == 4: - self.setFormat( 0, len(text), self.storyUrlRule.highlight ) - - self.setCurrentBlockState( blocknum ) - -class HighlightingRule(): - def __init__( self, pattern, color, - weight=QFont.Normal, - style=Qt.SolidPattern, - blocknum=0): - if isinstance(pattern,basestring): - self.pattern = re.compile(pattern) - else: - self.pattern=pattern - charfmt = QTextCharFormat() - brush = QBrush(color, style) - charfmt.setForeground(brush) - charfmt.setFontWeight(weight) - self.highlight = charfmt - self.blocknum=blocknum - +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2017, Jim Miller' +__docformat__ = 'restructuredtext en' + +import re + +try: + from PyQt5.Qt import (Qt, QSyntaxHighlighter, QTextCharFormat, QBrush, QFont) +except ImportError as e: + from PyQt4.Qt import (Qt, QSyntaxHighlighter, QTextCharFormat, QBrush, QFont) + +class IniHighlighter(QSyntaxHighlighter): + ''' + QSyntaxHighlighter class for use with QTextEdit for highlighting + ini config files. + ''' + + def __init__( self, parent, sections=[], keywords=[], entries=[], entry_keywords=[] ): + QSyntaxHighlighter.__init__( self, parent ) + self.parent = parent + + self.highlightingRules = [] + + if entries: + # *known* entries + reentries = r'('+(r'|'.join(entries))+r')' + self.highlightingRules.append( HighlightingRule( r"\b"+reentries+r"\b", Qt.darkGreen ) ) + + # true/false -- just to be nice. + self.highlightingRules.append( HighlightingRule( r"\b(true|false)\b", Qt.darkGreen ) ) + + # *all* keywords -- change known later. + self.errorRule = HighlightingRule( r"^[^:=\s][^:=]*[:=]", Qt.red ) + self.highlightingRules.append( self.errorRule ) + + # *all* entry keywords -- change known later. + reentrykeywords = r'('+(r'|'.join([ e % r'[a-zA-Z0-9_]+' for e in entry_keywords ]))+r')' + self.highlightingRules.append( HighlightingRule( r"^(add_to_)?"+reentrykeywords+r"(_filelist)?\s*[:=]", Qt.darkMagenta ) ) + + if entries: # separate from known entries so entry named keyword won't be masked. + # *known* entry keywords + reentrykeywords = r'('+(r'|'.join([ e % reentries for e in entry_keywords ]))+r')' + self.highlightingRules.append( HighlightingRule( r"^(add_to_)?"+reentrykeywords+r"(_filelist)?\s*[:=]", Qt.blue ) ) + + # *known* keywords + rekeywords = r'('+(r'|'.join(keywords))+r')' + self.highlightingRules.append( HighlightingRule( r"^(add_to_)?"+rekeywords+r"(_filelist)?\s*[:=]", Qt.blue ) ) + + # *all* sections -- change known later. + self.highlightingRules.append( HighlightingRule( r"^\[[^\]]+\].*?$", Qt.red, QFont.Bold, blocknum=1 ) ) + + if sections: + # *known* sections + resections = r'('+(r'|'.join(sections))+r')' + resections = resections.replace('.','\.') #escape dots. + self.highlightingRules.append( HighlightingRule( r"^\["+resections+r"\]\s*$", Qt.darkBlue, QFont.Bold, blocknum=2 ) ) + + # test story sections + self.teststoryRule = HighlightingRule( r"^\[teststory:([0-9]+|defaults)\]", Qt.darkCyan, blocknum=3 ) + self.highlightingRules.append( self.teststoryRule ) + + # storyUrl sections + self.storyUrlRule = HighlightingRule( r"^\[https?://.*\]", Qt.darkMagenta, blocknum=4 ) + self.highlightingRules.append( self.storyUrlRule ) + + # NOT comments -- but can be custom columns, so don't flag. + #self.highlightingRules.append( HighlightingRule( r"(? 0: + blocknum = rule.blocknum + + if not is_comment: + # unknown section, error all: + if blocknum == 1 and blocknum == self.previousBlockState(): + self.setFormat( 0, len(text), self.errorRule.highlight ) + + # teststory section rules: + if blocknum == 3: + self.setFormat( 0, len(text), self.teststoryRule.highlight ) + + # storyUrl section rules: + if blocknum == 4: + self.setFormat( 0, len(text), self.storyUrlRule.highlight ) + + self.setCurrentBlockState( blocknum ) + +class HighlightingRule(): + def __init__( self, pattern, color, + weight=QFont.Normal, + style=Qt.SolidPattern, + blocknum=0): + if isinstance(pattern,basestring): + self.pattern = re.compile(pattern) + else: + self.pattern=pattern + charfmt = QTextCharFormat() + brush = QBrush(color, style) + charfmt.setForeground(brush) + charfmt.setFontWeight(weight) + self.highlight = charfmt + self.blocknum=blocknum + diff --git a/calibre-plugin/jobs.py b/calibre-plugin/jobs.py index b7289258..a7353703 100644 --- a/calibre-plugin/jobs.py +++ b/calibre-plugin/jobs.py @@ -1,342 +1,342 @@ -# -*- coding: utf-8 -*- - -from __future__ import (unicode_literals, division, absolute_import, - print_function) - -__license__ = 'GPL v3' -__copyright__ = '2017, Jim Miller, 2011, Grant Drake ' -__docformat__ = 'restructuredtext en' - -import logging -logger = logging.getLogger(__name__) - -import traceback -from datetime import time -from StringIO import StringIO - -from calibre.utils.ipc.server import Server -from calibre.utils.ipc.job import ParallelJob -from calibre.constants import numeric_version as calibre_version -from calibre.utils.date import local_tz - -from calibre_plugins.fanficfare_plugin.wordcount import get_word_count -from calibre_plugins.fanficfare_plugin.prefs import (SAVE_YES, SAVE_YES_UNLESS_SITE) - -# pulls in translation files for _() strings -try: - load_translations() -except NameError: - pass # load_translations() added in calibre 1.9 - -# ------------------------------------------------------------------------------ -# -# Functions to perform downloads using worker jobs -# -# ------------------------------------------------------------------------------ - -def do_download_worker(book_list, - options, - cpus, - merge=False, - notification=lambda x,y:x): - ''' - Master job, to launch child jobs to extract ISBN for a set of books - This is run as a worker job in the background to keep the UI more - responsive and get around the memory leak issues as it will launch - a child job for each book as a worker process - ''' - server = Server(pool_size=cpus) - - logger.info(options['version']) - total = 0 - alreadybad = [] - # Queue all the jobs - logger.info("Adding jobs for URLs:") - for book in book_list: - logger.info("%s"%book['url']) - if book['good']: - total += 1 - args = ['calibre_plugins.fanficfare_plugin.jobs', - 'do_download_for_worker', - (book,options,merge)] - job = ParallelJob('arbitrary_n', - "url:(%s) id:(%s)"%(book['url'],book['calibre_id']), - done=None, - args=args) - job._book = book - server.add_job(job) - else: - # was already bad before the subprocess ever started. - alreadybad.append(book) - - # This server is an arbitrary_n job, so there is a notifier available. - # Set the % complete to a small number to avoid the 'unavailable' indicator - notification(0.01, _('Downloading FanFiction Stories')) - - # dequeue the job results as they arrive, saving the results - count = 0 - while True: - job = server.changed_jobs_queue.get() - # A job can 'change' when it is not finished, for example if it - # produces a notification. Ignore these. - job.update() - if not job.is_finished: - continue - # A job really finished. Get the information. - book_list.remove(job._book) - book_list.append(job.result) - book_id = job._book['calibre_id'] - count = count + 1 - notification(float(count)/total, _('%d of %d stories finished downloading')%(count,total)) - # Add this job's output to the current log - logger.info('Logfile for book ID %s (%s)'%(book_id, job._book['title'])) - logger.info(job.details) - - if count >= total: - ## ordering first by good vs bad, then by listorder. - good_list = filter(lambda x : x['good'], book_list) - bad_list = filter(lambda x : not x['good'], book_list) - good_list = sorted(good_list,key=lambda x : x['listorder']) - bad_list = sorted(bad_list,key=lambda x : x['listorder']) - - logger.info("\n"+_("Download Results:")+"\n%s\n"%("\n".join([ "%(url)s %(comment)s" % book for book in good_list+bad_list]))) - - logger.info("\n"+_("Successful:")+"\n%s\n"%("\n".join([book['url'] for book in good_list]))) - logger.info("\n"+_("Unsuccessful:")+"\n%s\n"%("\n".join([book['url'] for book in bad_list]))) - break - - server.close() - - # return the book list as the job result - return book_list - -def do_download_for_worker(book,options,merge,notification=lambda x,y:x): - ''' - Child job, to download story when run as a worker job - ''' - - from calibre_plugins.fanficfare_plugin import FanFicFareBase - fffbase = FanFicFareBase(options['plugin_path']) - with fffbase: - - from calibre_plugins.fanficfare_plugin.dialogs import (NotGoingToDownload, - OVERWRITE, OVERWRITEALWAYS, UPDATE, UPDATEALWAYS, ADDNEW, SKIP, CALIBREONLY, CALIBREONLYSAVECOL) - from calibre_plugins.fanficfare_plugin.fanficfare import adapters, writers, exceptions - from calibre_plugins.fanficfare_plugin.fanficfare.epubutils import get_update_data - - from calibre_plugins.fanficfare_plugin.fff_util import (get_fff_adapter, get_fff_config) - - try: - book['comment'] = _('Download started...') - - configuration = get_fff_config(book['url'], - options['fileform'], - options['personal.ini']) - - if configuration.getConfig('use_ssl_unverified_context'): - ## monkey patch to avoid SSL bug. dupliated from - ## fff_plugin.py because bg jobs run in own process - ## space. - import ssl - if hasattr(ssl, '_create_unverified_context'): - ssl._create_default_https_context = ssl._create_unverified_context - - if not options['updateepubcover'] and 'epub_for_update' in book and options['collision'] in (UPDATE, UPDATEALWAYS): - configuration.set("overrides","never_make_cover","true") - - # images only for epub, html, even if the user mistakenly - # turned it on else where. - if options['fileform'] not in ("epub","html"): - configuration.set("overrides","include_images","false") - - adapter = adapters.getAdapter(configuration,book['url']) - adapter.is_adult = book['is_adult'] - adapter.username = book['username'] - adapter.password = book['password'] - adapter.setChaptersRange(book['begin'],book['end']) - - configuration.load_cookiejar(options['cookiejarfile']) - #logger.debug("cookiejar:%s"%configuration.cookiejar) - configuration.set_pagecache(options['pagecache']) - - story = adapter.getStoryMetadataOnly() - if not story.getMetadata("series") and 'calibre_series' in book: - adapter.setSeries(book['calibre_series'][0],book['calibre_series'][1]) - - # set PI version instead of default. - if 'version' in options: - story.setMetadata('version',options['version']) - - book['title'] = story.getMetadata("title", removeallentities=True) - book['author_sort'] = book['author'] = story.getList("author", removeallentities=True) - book['publisher'] = story.getMetadata("site") - book['url'] = story.getMetadata("storyUrl") - book['tags'] = story.getSubjectTags(removeallentities=True) - book['comments'] = story.get_sanitized_description() - book['series'] = story.getMetadata("series", removeallentities=True) - - if story.getMetadataRaw('datePublished'): - book['pubdate'] = story.getMetadataRaw('datePublished').replace(tzinfo=local_tz) - if story.getMetadataRaw('dateUpdated'): - book['updatedate'] = story.getMetadataRaw('dateUpdated').replace(tzinfo=local_tz) - if story.getMetadataRaw('dateCreated'): - book['timestamp'] = story.getMetadataRaw('dateCreated').replace(tzinfo=local_tz) - else: - book['timestamp'] = datetime.now() # need *something* there for calibre. - - writer = writers.getWriter(options['fileform'],configuration,adapter) - - outfile = book['outfile'] - - ## No need to download at all. Shouldn't ever get down here. - if options['collision'] in (CALIBREONLY, CALIBREONLYSAVECOL): - logger.info("Skipping CALIBREONLY 'update' down inside worker--this shouldn't be happening...") - book['comment'] = _('Metadata collected.') - book['all_metadata'] = story.getAllMetadata(removeallentities=True) - if options['savemetacol'] != '': - book['savemetacol'] = story.dump_html_metadata() - - ## checks were done earlier, it's new or not dup or newer--just write it. - elif options['collision'] in (ADDNEW, SKIP, OVERWRITE, OVERWRITEALWAYS) or \ - ('epub_for_update' not in book and options['collision'] in (UPDATE, UPDATEALWAYS)): - - # preserve logfile even on overwrite. - if 'epub_for_update' in book: - adapter.logfile = get_update_data(book['epub_for_update'])[6] - # change the existing entries id to notid so - # write_epub writes a whole new set to indicate overwrite. - if adapter.logfile: - adapter.logfile = adapter.logfile.replace("span id","span notid") - - if options['collision'] == OVERWRITE and 'fileupdated' in book: - lastupdated=story.getMetadataRaw('dateUpdated') - fileupdated=book['fileupdated'] - - # updated doesn't have time (or is midnight), use dates only. - # updated does have time, use full timestamps. - if (lastupdated.time() == time.min and fileupdated.date() > lastupdated.date()) or \ - (lastupdated.time() != time.min and fileupdated > lastupdated): - raise NotGoingToDownload(_("Not Overwriting, web site is not newer."),'edit-undo.png',showerror=False) - - - logger.info("write to %s"%outfile) - inject_cal_cols(book,story,configuration) - writer.writeStory(outfilename=outfile, forceOverwrite=True) - - book['comment'] = _('Download %s completed, %s chapters.')%(options['fileform'],story.getMetadata("numChapters")) - book['all_metadata'] = story.getAllMetadata(removeallentities=True) - if options['savemetacol'] != '': - book['savemetacol'] = story.dump_html_metadata() - - ## checks were done earlier, just update it. - elif 'epub_for_update' in book and options['collision'] in (UPDATE, UPDATEALWAYS): - - # update now handled by pre-populating the old images and - # chapters in the adapter rather than merging epubs. - #urlchaptercount = int(story.getMetadata('numChapters').replace(',','')) - # returns int adjusted for start-end range. - urlchaptercount = story.getChapterCount() - (url, - chaptercount, - adapter.oldchapters, - adapter.oldimgs, - adapter.oldcover, - adapter.calibrebookmark, - adapter.logfile, - adapter.oldchaptersmap, - adapter.oldchaptersdata) = get_update_data(book['epub_for_update'])[0:9] - - # dup handling from fff_plugin needed for anthology updates. - if options['collision'] == UPDATE: - if chaptercount == urlchaptercount: - if merge: - book['comment']=_("Already contains %d chapters. Reuse as is.")%chaptercount - book['all_metadata'] = story.getAllMetadata(removeallentities=True) - if options['savemetacol'] != '': - book['savemetacol'] = story.dump_html_metadata() - book['outfile'] = book['epub_for_update'] # for anthology merge ops. - return book - else: # not merge, - raise NotGoingToDownload(_("Already contains %d chapters.")%chaptercount,'edit-undo.png',showerror=False) - elif chaptercount > urlchaptercount: - raise NotGoingToDownload(_("Existing epub contains %d chapters, web site only has %d. Use Overwrite to force update.") % (chaptercount,urlchaptercount),'dialog_error.png') - elif chaptercount == 0: - raise NotGoingToDownload(_("FanFicFare doesn't recognize chapters in existing epub, epub is probably from a different source. Use Overwrite to force update."),'dialog_error.png') - - if not (options['collision'] == UPDATEALWAYS and chaptercount == urlchaptercount) \ - and adapter.getConfig("do_update_hook"): - chaptercount = adapter.hookForUpdates(chaptercount) - - logger.info("Do update - epub(%d) vs url(%d)" % (chaptercount, urlchaptercount)) - logger.info("write to %s"%outfile) - - inject_cal_cols(book,story,configuration) - writer.writeStory(outfilename=outfile, forceOverwrite=True) - - book['comment'] = _('Update %s completed, added %s chapters for %s total.')%\ - (options['fileform'],(urlchaptercount-chaptercount),urlchaptercount) - book['all_metadata'] = story.getAllMetadata(removeallentities=True) - if options['savemetacol'] != '': - book['savemetacol'] = story.dump_html_metadata() - - if options['do_wordcount'] == SAVE_YES or ( - options['do_wordcount'] == SAVE_YES_UNLESS_SITE and not story.getMetadataRaw('numWords') ): - wordcount = get_word_count(outfile) - logger.info("get_word_count:%s"%wordcount) - story.setMetadata('numWords',wordcount) - writer.writeStory(outfilename=outfile, forceOverwrite=True) - book['all_metadata'] = story.getAllMetadata(removeallentities=True) - if options['savemetacol'] != '': - book['savemetacol'] = story.dump_html_metadata() - - if options['smarten_punctuation'] and options['fileform'] == "epub" \ - and calibre_version >= (0, 9, 39): - # for smarten punc - from calibre.ebooks.oeb.polish.main import polish, ALL_OPTS - from calibre.utils.logging import Log - from collections import namedtuple - - # do smarten_punctuation from calibre's polish feature - data = {'smarten_punctuation':True} - opts = ALL_OPTS.copy() - opts.update(data) - O = namedtuple('Options', ' '.join(ALL_OPTS.iterkeys())) - opts = O(**opts) - - log = Log(level=Log.DEBUG) - polish({outfile:outfile}, opts, log, logger.info) - - except NotGoingToDownload as d: - book['good']=False - book['showerror']=d.showerror - book['comment']=unicode(d) - book['icon'] = d.icon - - except Exception as e: - book['good']=False - book['comment']=unicode(e) - book['icon']='dialog_error.png' - book['status'] = _('Error') - logger.info("Exception: %s:%s"%(book,unicode(e)),exc_info=True) - - #time.sleep(10) - return book - -## calibre's columns for an existing book are passed in and injected -## into the story's metadata. For convenience, we also add labels and -## valid_entries for them in a special [injected] section that has -## even less precedence than [defaults] -def inject_cal_cols(book,story,configuration): - configuration.remove_section('injected') - if 'calibre_columns' in book: - injectini = ['[injected]'] - extra_valid = [] - for k, v in book['calibre_columns'].iteritems(): - story.setMetadata(k,v['val']) - injectini.append('%s_label:%s'%(k,v['label'])) - extra_valid.append(k) - if extra_valid: # if empty, there's nothing to add. - injectini.append("add_to_extra_valid_entries:,"+','.join(extra_valid)) - configuration.readfp(StringIO('\n'.join(injectini))) - #print("added:\n%s\n"%('\n'.join(injectini))) - +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2017, Jim Miller, 2011, Grant Drake ' +__docformat__ = 'restructuredtext en' + +import logging +logger = logging.getLogger(__name__) + +import traceback +from datetime import time +from StringIO import StringIO + +from calibre.utils.ipc.server import Server +from calibre.utils.ipc.job import ParallelJob +from calibre.constants import numeric_version as calibre_version +from calibre.utils.date import local_tz + +from calibre_plugins.fanficfare_plugin.wordcount import get_word_count +from calibre_plugins.fanficfare_plugin.prefs import (SAVE_YES, SAVE_YES_UNLESS_SITE) + +# pulls in translation files for _() strings +try: + load_translations() +except NameError: + pass # load_translations() added in calibre 1.9 + +# ------------------------------------------------------------------------------ +# +# Functions to perform downloads using worker jobs +# +# ------------------------------------------------------------------------------ + +def do_download_worker(book_list, + options, + cpus, + merge=False, + notification=lambda x,y:x): + ''' + Master job, to launch child jobs to extract ISBN for a set of books + This is run as a worker job in the background to keep the UI more + responsive and get around the memory leak issues as it will launch + a child job for each book as a worker process + ''' + server = Server(pool_size=cpus) + + logger.info(options['version']) + total = 0 + alreadybad = [] + # Queue all the jobs + logger.info("Adding jobs for URLs:") + for book in book_list: + logger.info("%s"%book['url']) + if book['good']: + total += 1 + args = ['calibre_plugins.fanficfare_plugin.jobs', + 'do_download_for_worker', + (book,options,merge)] + job = ParallelJob('arbitrary_n', + "url:(%s) id:(%s)"%(book['url'],book['calibre_id']), + done=None, + args=args) + job._book = book + server.add_job(job) + else: + # was already bad before the subprocess ever started. + alreadybad.append(book) + + # This server is an arbitrary_n job, so there is a notifier available. + # Set the % complete to a small number to avoid the 'unavailable' indicator + notification(0.01, _('Downloading FanFiction Stories')) + + # dequeue the job results as they arrive, saving the results + count = 0 + while True: + job = server.changed_jobs_queue.get() + # A job can 'change' when it is not finished, for example if it + # produces a notification. Ignore these. + job.update() + if not job.is_finished: + continue + # A job really finished. Get the information. + book_list.remove(job._book) + book_list.append(job.result) + book_id = job._book['calibre_id'] + count = count + 1 + notification(float(count)/total, _('%d of %d stories finished downloading')%(count,total)) + # Add this job's output to the current log + logger.info('Logfile for book ID %s (%s)'%(book_id, job._book['title'])) + logger.info(job.details) + + if count >= total: + ## ordering first by good vs bad, then by listorder. + good_list = filter(lambda x : x['good'], book_list) + bad_list = filter(lambda x : not x['good'], book_list) + good_list = sorted(good_list,key=lambda x : x['listorder']) + bad_list = sorted(bad_list,key=lambda x : x['listorder']) + + logger.info("\n"+_("Download Results:")+"\n%s\n"%("\n".join([ "%(url)s %(comment)s" % book for book in good_list+bad_list]))) + + logger.info("\n"+_("Successful:")+"\n%s\n"%("\n".join([book['url'] for book in good_list]))) + logger.info("\n"+_("Unsuccessful:")+"\n%s\n"%("\n".join([book['url'] for book in bad_list]))) + break + + server.close() + + # return the book list as the job result + return book_list + +def do_download_for_worker(book,options,merge,notification=lambda x,y:x): + ''' + Child job, to download story when run as a worker job + ''' + + from calibre_plugins.fanficfare_plugin import FanFicFareBase + fffbase = FanFicFareBase(options['plugin_path']) + with fffbase: + + from calibre_plugins.fanficfare_plugin.dialogs import (NotGoingToDownload, + OVERWRITE, OVERWRITEALWAYS, UPDATE, UPDATEALWAYS, ADDNEW, SKIP, CALIBREONLY, CALIBREONLYSAVECOL) + from calibre_plugins.fanficfare_plugin.fanficfare import adapters, writers, exceptions + from calibre_plugins.fanficfare_plugin.fanficfare.epubutils import get_update_data + + from calibre_plugins.fanficfare_plugin.fff_util import (get_fff_adapter, get_fff_config) + + try: + book['comment'] = _('Download started...') + + configuration = get_fff_config(book['url'], + options['fileform'], + options['personal.ini']) + + if configuration.getConfig('use_ssl_unverified_context'): + ## monkey patch to avoid SSL bug. dupliated from + ## fff_plugin.py because bg jobs run in own process + ## space. + import ssl + if hasattr(ssl, '_create_unverified_context'): + ssl._create_default_https_context = ssl._create_unverified_context + + if not options['updateepubcover'] and 'epub_for_update' in book and options['collision'] in (UPDATE, UPDATEALWAYS): + configuration.set("overrides","never_make_cover","true") + + # images only for epub, html, even if the user mistakenly + # turned it on else where. + if options['fileform'] not in ("epub","html"): + configuration.set("overrides","include_images","false") + + adapter = adapters.getAdapter(configuration,book['url']) + adapter.is_adult = book['is_adult'] + adapter.username = book['username'] + adapter.password = book['password'] + adapter.setChaptersRange(book['begin'],book['end']) + + configuration.load_cookiejar(options['cookiejarfile']) + #logger.debug("cookiejar:%s"%configuration.cookiejar) + configuration.set_pagecache(options['pagecache']) + + story = adapter.getStoryMetadataOnly() + if not story.getMetadata("series") and 'calibre_series' in book: + adapter.setSeries(book['calibre_series'][0],book['calibre_series'][1]) + + # set PI version instead of default. + if 'version' in options: + story.setMetadata('version',options['version']) + + book['title'] = story.getMetadata("title", removeallentities=True) + book['author_sort'] = book['author'] = story.getList("author", removeallentities=True) + book['publisher'] = story.getMetadata("site") + book['url'] = story.getMetadata("storyUrl") + book['tags'] = story.getSubjectTags(removeallentities=True) + book['comments'] = story.get_sanitized_description() + book['series'] = story.getMetadata("series", removeallentities=True) + + if story.getMetadataRaw('datePublished'): + book['pubdate'] = story.getMetadataRaw('datePublished').replace(tzinfo=local_tz) + if story.getMetadataRaw('dateUpdated'): + book['updatedate'] = story.getMetadataRaw('dateUpdated').replace(tzinfo=local_tz) + if story.getMetadataRaw('dateCreated'): + book['timestamp'] = story.getMetadataRaw('dateCreated').replace(tzinfo=local_tz) + else: + book['timestamp'] = datetime.now() # need *something* there for calibre. + + writer = writers.getWriter(options['fileform'],configuration,adapter) + + outfile = book['outfile'] + + ## No need to download at all. Shouldn't ever get down here. + if options['collision'] in (CALIBREONLY, CALIBREONLYSAVECOL): + logger.info("Skipping CALIBREONLY 'update' down inside worker--this shouldn't be happening...") + book['comment'] = _('Metadata collected.') + book['all_metadata'] = story.getAllMetadata(removeallentities=True) + if options['savemetacol'] != '': + book['savemetacol'] = story.dump_html_metadata() + + ## checks were done earlier, it's new or not dup or newer--just write it. + elif options['collision'] in (ADDNEW, SKIP, OVERWRITE, OVERWRITEALWAYS) or \ + ('epub_for_update' not in book and options['collision'] in (UPDATE, UPDATEALWAYS)): + + # preserve logfile even on overwrite. + if 'epub_for_update' in book: + adapter.logfile = get_update_data(book['epub_for_update'])[6] + # change the existing entries id to notid so + # write_epub writes a whole new set to indicate overwrite. + if adapter.logfile: + adapter.logfile = adapter.logfile.replace("span id","span notid") + + if options['collision'] == OVERWRITE and 'fileupdated' in book: + lastupdated=story.getMetadataRaw('dateUpdated') + fileupdated=book['fileupdated'] + + # updated doesn't have time (or is midnight), use dates only. + # updated does have time, use full timestamps. + if (lastupdated.time() == time.min and fileupdated.date() > lastupdated.date()) or \ + (lastupdated.time() != time.min and fileupdated > lastupdated): + raise NotGoingToDownload(_("Not Overwriting, web site is not newer."),'edit-undo.png',showerror=False) + + + logger.info("write to %s"%outfile) + inject_cal_cols(book,story,configuration) + writer.writeStory(outfilename=outfile, forceOverwrite=True) + + book['comment'] = _('Download %s completed, %s chapters.')%(options['fileform'],story.getMetadata("numChapters")) + book['all_metadata'] = story.getAllMetadata(removeallentities=True) + if options['savemetacol'] != '': + book['savemetacol'] = story.dump_html_metadata() + + ## checks were done earlier, just update it. + elif 'epub_for_update' in book and options['collision'] in (UPDATE, UPDATEALWAYS): + + # update now handled by pre-populating the old images and + # chapters in the adapter rather than merging epubs. + #urlchaptercount = int(story.getMetadata('numChapters').replace(',','')) + # returns int adjusted for start-end range. + urlchaptercount = story.getChapterCount() + (url, + chaptercount, + adapter.oldchapters, + adapter.oldimgs, + adapter.oldcover, + adapter.calibrebookmark, + adapter.logfile, + adapter.oldchaptersmap, + adapter.oldchaptersdata) = get_update_data(book['epub_for_update'])[0:9] + + # dup handling from fff_plugin needed for anthology updates. + if options['collision'] == UPDATE: + if chaptercount == urlchaptercount: + if merge: + book['comment']=_("Already contains %d chapters. Reuse as is.")%chaptercount + book['all_metadata'] = story.getAllMetadata(removeallentities=True) + if options['savemetacol'] != '': + book['savemetacol'] = story.dump_html_metadata() + book['outfile'] = book['epub_for_update'] # for anthology merge ops. + return book + else: # not merge, + raise NotGoingToDownload(_("Already contains %d chapters.")%chaptercount,'edit-undo.png',showerror=False) + elif chaptercount > urlchaptercount: + raise NotGoingToDownload(_("Existing epub contains %d chapters, web site only has %d. Use Overwrite to force update.") % (chaptercount,urlchaptercount),'dialog_error.png') + elif chaptercount == 0: + raise NotGoingToDownload(_("FanFicFare doesn't recognize chapters in existing epub, epub is probably from a different source. Use Overwrite to force update."),'dialog_error.png') + + if not (options['collision'] == UPDATEALWAYS and chaptercount == urlchaptercount) \ + and adapter.getConfig("do_update_hook"): + chaptercount = adapter.hookForUpdates(chaptercount) + + logger.info("Do update - epub(%d) vs url(%d)" % (chaptercount, urlchaptercount)) + logger.info("write to %s"%outfile) + + inject_cal_cols(book,story,configuration) + writer.writeStory(outfilename=outfile, forceOverwrite=True) + + book['comment'] = _('Update %s completed, added %s chapters for %s total.')%\ + (options['fileform'],(urlchaptercount-chaptercount),urlchaptercount) + book['all_metadata'] = story.getAllMetadata(removeallentities=True) + if options['savemetacol'] != '': + book['savemetacol'] = story.dump_html_metadata() + + if options['do_wordcount'] == SAVE_YES or ( + options['do_wordcount'] == SAVE_YES_UNLESS_SITE and not story.getMetadataRaw('numWords') ): + wordcount = get_word_count(outfile) + logger.info("get_word_count:%s"%wordcount) + story.setMetadata('numWords',wordcount) + writer.writeStory(outfilename=outfile, forceOverwrite=True) + book['all_metadata'] = story.getAllMetadata(removeallentities=True) + if options['savemetacol'] != '': + book['savemetacol'] = story.dump_html_metadata() + + if options['smarten_punctuation'] and options['fileform'] == "epub" \ + and calibre_version >= (0, 9, 39): + # for smarten punc + from calibre.ebooks.oeb.polish.main import polish, ALL_OPTS + from calibre.utils.logging import Log + from collections import namedtuple + + # do smarten_punctuation from calibre's polish feature + data = {'smarten_punctuation':True} + opts = ALL_OPTS.copy() + opts.update(data) + O = namedtuple('Options', ' '.join(ALL_OPTS.iterkeys())) + opts = O(**opts) + + log = Log(level=Log.DEBUG) + polish({outfile:outfile}, opts, log, logger.info) + + except NotGoingToDownload as d: + book['good']=False + book['showerror']=d.showerror + book['comment']=unicode(d) + book['icon'] = d.icon + + except Exception as e: + book['good']=False + book['comment']=unicode(e) + book['icon']='dialog_error.png' + book['status'] = _('Error') + logger.info("Exception: %s:%s"%(book,unicode(e)),exc_info=True) + + #time.sleep(10) + return book + +## calibre's columns for an existing book are passed in and injected +## into the story's metadata. For convenience, we also add labels and +## valid_entries for them in a special [injected] section that has +## even less precedence than [defaults] +def inject_cal_cols(book,story,configuration): + configuration.remove_section('injected') + if 'calibre_columns' in book: + injectini = ['[injected]'] + extra_valid = [] + for k, v in book['calibre_columns'].iteritems(): + story.setMetadata(k,v['val']) + injectini.append('%s_label:%s'%(k,v['label'])) + extra_valid.append(k) + if extra_valid: # if empty, there's nothing to add. + injectini.append("add_to_extra_valid_entries:,"+','.join(extra_valid)) + configuration.readfp(StringIO('\n'.join(injectini))) + #print("added:\n%s\n"%('\n'.join(injectini))) + diff --git a/calibre-plugin/prefs.py b/calibre-plugin/prefs.py index bba1f518..bf04956b 100644 --- a/calibre-plugin/prefs.py +++ b/calibre-plugin/prefs.py @@ -1,263 +1,263 @@ -# -*- coding: utf-8 -*- - -from __future__ import (unicode_literals, division, absolute_import, - print_function) - -__license__ = 'GPL v3' -__copyright__ = '2016, Jim Miller' -__docformat__ = 'restructuredtext en' - -import logging -logger = logging.getLogger(__name__) - -import copy - -from calibre.utils.config import JSONConfig -from calibre.gui2.ui import get_gui - -from calibre_plugins.fanficfare_plugin import __version__ as plugin_version -from calibre_plugins.fanficfare_plugin.common_utils import get_library_uuid - -SKIP=_('Skip') -ADDNEW=_('Add New Book') -UPDATE=_('Update EPUB if New Chapters') -UPDATEALWAYS=_('Update EPUB Always') -OVERWRITE=_('Overwrite if Newer') -OVERWRITEALWAYS=_('Overwrite Always') -CALIBREONLY=_('Update Calibre Metadata from Web Site') -CALIBREONLYSAVECOL=_('Update Calibre Metadata from Saved Metadata Column') -collision_order=[SKIP, - ADDNEW, - UPDATE, - UPDATEALWAYS, - OVERWRITE, - OVERWRITEALWAYS, - CALIBREONLY, - CALIBREONLYSAVECOL,] - -# best idea I've had for how to deal with config/pref saving the -# collision name in english. -SAVE_SKIP='Skip' -SAVE_ADDNEW='Add New Book' -SAVE_UPDATE='Update EPUB if New Chapters' -SAVE_UPDATEALWAYS='Update EPUB Always' -SAVE_OVERWRITE='Overwrite if Newer' -SAVE_OVERWRITEALWAYS='Overwrite Always' -SAVE_CALIBREONLY='Update Calibre Metadata Only' -SAVE_CALIBREONLYSAVECOL='Update Calibre Metadata Only(Saved Column)' -save_collisions={ - SKIP:SAVE_SKIP, - ADDNEW:SAVE_ADDNEW, - UPDATE:SAVE_UPDATE, - UPDATEALWAYS:SAVE_UPDATEALWAYS, - OVERWRITE:SAVE_OVERWRITE, - OVERWRITEALWAYS:SAVE_OVERWRITEALWAYS, - CALIBREONLY:SAVE_CALIBREONLY, - CALIBREONLYSAVECOL:SAVE_CALIBREONLYSAVECOL, - SAVE_SKIP:SKIP, - SAVE_ADDNEW:ADDNEW, - SAVE_UPDATE:UPDATE, - SAVE_UPDATEALWAYS:UPDATEALWAYS, - SAVE_OVERWRITE:OVERWRITE, - SAVE_OVERWRITEALWAYS:OVERWRITEALWAYS, - SAVE_CALIBREONLY:CALIBREONLY, - SAVE_CALIBREONLYSAVECOL:CALIBREONLYSAVECOL, - } - -anthology_collision_order=[UPDATE, - UPDATEALWAYS, - OVERWRITEALWAYS] - - -# Show translated strings, but save the same string in prefs so your -# prefs are the same in different languages. -YES=_('Yes, Always') -SAVE_YES='Yes' -YES_IF_IMG=_('Yes, if EPUB has a cover image') -SAVE_YES_IF_IMG='Yes, if img' -YES_UNLESS_IMG=_('Yes, unless FanFicFare found a cover image') -SAVE_YES_UNLESS_IMG='Yes, unless img' -YES_UNLESS_SITE=_('Yes, unless found on site') -SAVE_YES_UNLESS_SITE='Yes, unless site' -NO=_('No') -SAVE_NO='No' -prefs_save_options = { - YES:SAVE_YES, - SAVE_YES:YES, - YES_IF_IMG:SAVE_YES_IF_IMG, - SAVE_YES_IF_IMG:YES_IF_IMG, - YES_UNLESS_IMG:SAVE_YES_UNLESS_IMG, - SAVE_YES_UNLESS_IMG:YES_UNLESS_IMG, - NO:SAVE_NO, - SAVE_NO:NO, - YES_UNLESS_SITE:SAVE_YES_UNLESS_SITE, - SAVE_YES_UNLESS_SITE:YES_UNLESS_SITE, - } -updatecalcover_order=[YES,YES_IF_IMG,NO] -gencalcover_order=[YES,YES_UNLESS_IMG,NO] -do_wordcount_order=[YES,YES_UNLESS_SITE,NO] - -# if don't have any settings for FanFicFarePlugin, copy from -# predecessor FanFictionDownLoaderPlugin. -FFDL_PREFS_NAMESPACE = 'FanFictionDownLoaderPlugin' -PREFS_NAMESPACE = 'FanFicFarePlugin' -PREFS_KEY_SETTINGS = 'settings' - -# Set defaults used by all. Library specific settings continue to -# take from here. -default_prefs = {} -default_prefs['last_saved_version'] = (0,0,0) -default_prefs['personal.ini'] = get_resources('plugin-example.ini') -default_prefs['cal_cols_pass_in'] = False -default_prefs['rejecturls'] = '' -default_prefs['rejectreasons'] = '''Sucked -Boring -Dup from another site''' -default_prefs['reject_always'] = False -default_prefs['reject_delete_default'] = True - -default_prefs['updatemeta'] = True -default_prefs['bgmeta'] = False -default_prefs['updateepubcover'] = False -default_prefs['keeptags'] = False -default_prefs['suppressauthorsort'] = False -default_prefs['suppresstitlesort'] = False -default_prefs['mark'] = False -default_prefs['showmarked'] = False -default_prefs['autoconvert'] = False -default_prefs['urlsfromclip'] = True -default_prefs['updatedefault'] = True -default_prefs['fileform'] = 'epub' -default_prefs['collision'] = SAVE_UPDATE -default_prefs['deleteotherforms'] = False -default_prefs['adddialogstaysontop'] = False -default_prefs['lookforurlinhtml'] = False -default_prefs['checkforseriesurlid'] = True -default_prefs['auto_reject_seriesurlid'] = False -default_prefs['checkforurlchange'] = True -default_prefs['injectseries'] = False -default_prefs['matchtitleauth'] = True -default_prefs['do_wordcount'] = SAVE_YES_UNLESS_SITE -default_prefs['smarten_punctuation'] = False -default_prefs['show_est_time'] = False - -default_prefs['send_lists'] = '' -default_prefs['read_lists'] = '' -default_prefs['addtolists'] = False -default_prefs['addtoreadlists'] = False -default_prefs['addtolistsonread'] = False -default_prefs['autounnew'] = False - -default_prefs['updatecalcover'] = None -default_prefs['gencalcover'] = SAVE_YES -default_prefs['updatecover'] = False -default_prefs['calibre_gen_cover'] = False -default_prefs['plugin_gen_cover'] = True -default_prefs['gcnewonly'] = False -default_prefs['gc_site_settings'] = {} -default_prefs['allow_gc_from_ini'] = True -default_prefs['gc_polish_cover'] = False - -default_prefs['countpagesstats'] = [] -default_prefs['wordcountmissing'] = False - -default_prefs['errorcol'] = '' -default_prefs['save_all_errors'] = True -default_prefs['savemetacol'] = '' -default_prefs['lastcheckedcol'] = '' -default_prefs['custom_cols'] = {} -default_prefs['custom_cols_newonly'] = {} -default_prefs['allow_custcol_from_ini'] = True - -default_prefs['std_cols_newonly'] = {} -default_prefs['set_author_url'] = True -default_prefs['includecomments'] = False -default_prefs['anth_comments_newonly'] = True - -default_prefs['imapserver'] = '' -default_prefs['imapuser'] = '' -default_prefs['imappass'] = '' -default_prefs['imapsessionpass'] = False -default_prefs['imapfolder'] = 'INBOX' -default_prefs['imapmarkread'] = True -default_prefs['auto_reject_from_email'] = False -default_prefs['update_existing_only_from_email'] = False -default_prefs['download_from_email_immediately'] = False - -def set_library_config(library_config,db): - db.prefs.set_namespaced(PREFS_NAMESPACE, - PREFS_KEY_SETTINGS, - library_config) - -def get_library_config(db): - library_id = get_library_uuid(db) - library_config = None - - if library_config is None: - #print("get prefs from db") - library_config = db.prefs.get_namespaced(PREFS_NAMESPACE, - PREFS_KEY_SETTINGS) - - # if don't have any settings for FanFicFarePlugin, copy from - # predecessor FanFictionDownLoaderPlugin. - if library_config is None: - logger.info("Attempting to read settings from predecessor--FFDL") - library_config = db.prefs.get_namespaced(FFDL_PREFS_NAMESPACE, - PREFS_KEY_SETTINGS) - if library_config is None: - # defaults. - logger.info("Using default settings") - library_config = copy.deepcopy(default_prefs) - - return library_config - -# 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 _get_db(self): - if self.passed_db: - return self.passed_db - else: - # In the GUI plugin we want current db so we detect when - # it's changed. CLI plugin calls need to pass db in. - return get_gui().current_db - - def __init__(self,passed_db=None): - self.default_prefs = default_prefs - self.libraryid = None - self.current_prefs = None - self.passed_db=passed_db - - def _get_prefs(self): - libraryid = get_library_uuid(self._get_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 - self.current_prefs = get_library_config(self._get_db()) - return self.current_prefs - - def __getitem__(self,k): - prefs = self._get_prefs() - if k not in prefs: - # pulls from default_prefs.defaults automatically if not set - # in default_prefs - return self.default_prefs[k] - return prefs[k] - - def __setitem__(self,k,v): - prefs = self._get_prefs() - prefs[k]=v - # self._save_prefs(prefs) - - def __delitem__(self,k): - prefs = self._get_prefs() - if k in prefs: - del prefs[k] - - def save_to_db(self): - self['last_saved_version'] = plugin_version - set_library_config(self._get_prefs(),self._get_db()) - -prefs = PrefsFacade() - +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2016, Jim Miller' +__docformat__ = 'restructuredtext en' + +import logging +logger = logging.getLogger(__name__) + +import copy + +from calibre.utils.config import JSONConfig +from calibre.gui2.ui import get_gui + +from calibre_plugins.fanficfare_plugin import __version__ as plugin_version +from calibre_plugins.fanficfare_plugin.common_utils import get_library_uuid + +SKIP=_('Skip') +ADDNEW=_('Add New Book') +UPDATE=_('Update EPUB if New Chapters') +UPDATEALWAYS=_('Update EPUB Always') +OVERWRITE=_('Overwrite if Newer') +OVERWRITEALWAYS=_('Overwrite Always') +CALIBREONLY=_('Update Calibre Metadata from Web Site') +CALIBREONLYSAVECOL=_('Update Calibre Metadata from Saved Metadata Column') +collision_order=[SKIP, + ADDNEW, + UPDATE, + UPDATEALWAYS, + OVERWRITE, + OVERWRITEALWAYS, + CALIBREONLY, + CALIBREONLYSAVECOL,] + +# best idea I've had for how to deal with config/pref saving the +# collision name in english. +SAVE_SKIP='Skip' +SAVE_ADDNEW='Add New Book' +SAVE_UPDATE='Update EPUB if New Chapters' +SAVE_UPDATEALWAYS='Update EPUB Always' +SAVE_OVERWRITE='Overwrite if Newer' +SAVE_OVERWRITEALWAYS='Overwrite Always' +SAVE_CALIBREONLY='Update Calibre Metadata Only' +SAVE_CALIBREONLYSAVECOL='Update Calibre Metadata Only(Saved Column)' +save_collisions={ + SKIP:SAVE_SKIP, + ADDNEW:SAVE_ADDNEW, + UPDATE:SAVE_UPDATE, + UPDATEALWAYS:SAVE_UPDATEALWAYS, + OVERWRITE:SAVE_OVERWRITE, + OVERWRITEALWAYS:SAVE_OVERWRITEALWAYS, + CALIBREONLY:SAVE_CALIBREONLY, + CALIBREONLYSAVECOL:SAVE_CALIBREONLYSAVECOL, + SAVE_SKIP:SKIP, + SAVE_ADDNEW:ADDNEW, + SAVE_UPDATE:UPDATE, + SAVE_UPDATEALWAYS:UPDATEALWAYS, + SAVE_OVERWRITE:OVERWRITE, + SAVE_OVERWRITEALWAYS:OVERWRITEALWAYS, + SAVE_CALIBREONLY:CALIBREONLY, + SAVE_CALIBREONLYSAVECOL:CALIBREONLYSAVECOL, + } + +anthology_collision_order=[UPDATE, + UPDATEALWAYS, + OVERWRITEALWAYS] + + +# Show translated strings, but save the same string in prefs so your +# prefs are the same in different languages. +YES=_('Yes, Always') +SAVE_YES='Yes' +YES_IF_IMG=_('Yes, if EPUB has a cover image') +SAVE_YES_IF_IMG='Yes, if img' +YES_UNLESS_IMG=_('Yes, unless FanFicFare found a cover image') +SAVE_YES_UNLESS_IMG='Yes, unless img' +YES_UNLESS_SITE=_('Yes, unless found on site') +SAVE_YES_UNLESS_SITE='Yes, unless site' +NO=_('No') +SAVE_NO='No' +prefs_save_options = { + YES:SAVE_YES, + SAVE_YES:YES, + YES_IF_IMG:SAVE_YES_IF_IMG, + SAVE_YES_IF_IMG:YES_IF_IMG, + YES_UNLESS_IMG:SAVE_YES_UNLESS_IMG, + SAVE_YES_UNLESS_IMG:YES_UNLESS_IMG, + NO:SAVE_NO, + SAVE_NO:NO, + YES_UNLESS_SITE:SAVE_YES_UNLESS_SITE, + SAVE_YES_UNLESS_SITE:YES_UNLESS_SITE, + } +updatecalcover_order=[YES,YES_IF_IMG,NO] +gencalcover_order=[YES,YES_UNLESS_IMG,NO] +do_wordcount_order=[YES,YES_UNLESS_SITE,NO] + +# if don't have any settings for FanFicFarePlugin, copy from +# predecessor FanFictionDownLoaderPlugin. +FFDL_PREFS_NAMESPACE = 'FanFictionDownLoaderPlugin' +PREFS_NAMESPACE = 'FanFicFarePlugin' +PREFS_KEY_SETTINGS = 'settings' + +# Set defaults used by all. Library specific settings continue to +# take from here. +default_prefs = {} +default_prefs['last_saved_version'] = (0,0,0) +default_prefs['personal.ini'] = get_resources('plugin-example.ini') +default_prefs['cal_cols_pass_in'] = False +default_prefs['rejecturls'] = '' +default_prefs['rejectreasons'] = '''Sucked +Boring +Dup from another site''' +default_prefs['reject_always'] = False +default_prefs['reject_delete_default'] = True + +default_prefs['updatemeta'] = True +default_prefs['bgmeta'] = False +default_prefs['updateepubcover'] = False +default_prefs['keeptags'] = False +default_prefs['suppressauthorsort'] = False +default_prefs['suppresstitlesort'] = False +default_prefs['mark'] = False +default_prefs['showmarked'] = False +default_prefs['autoconvert'] = False +default_prefs['urlsfromclip'] = True +default_prefs['updatedefault'] = True +default_prefs['fileform'] = 'epub' +default_prefs['collision'] = SAVE_UPDATE +default_prefs['deleteotherforms'] = False +default_prefs['adddialogstaysontop'] = False +default_prefs['lookforurlinhtml'] = False +default_prefs['checkforseriesurlid'] = True +default_prefs['auto_reject_seriesurlid'] = False +default_prefs['checkforurlchange'] = True +default_prefs['injectseries'] = False +default_prefs['matchtitleauth'] = True +default_prefs['do_wordcount'] = SAVE_YES_UNLESS_SITE +default_prefs['smarten_punctuation'] = False +default_prefs['show_est_time'] = False + +default_prefs['send_lists'] = '' +default_prefs['read_lists'] = '' +default_prefs['addtolists'] = False +default_prefs['addtoreadlists'] = False +default_prefs['addtolistsonread'] = False +default_prefs['autounnew'] = False + +default_prefs['updatecalcover'] = None +default_prefs['gencalcover'] = SAVE_YES +default_prefs['updatecover'] = False +default_prefs['calibre_gen_cover'] = False +default_prefs['plugin_gen_cover'] = True +default_prefs['gcnewonly'] = False +default_prefs['gc_site_settings'] = {} +default_prefs['allow_gc_from_ini'] = True +default_prefs['gc_polish_cover'] = False + +default_prefs['countpagesstats'] = [] +default_prefs['wordcountmissing'] = False + +default_prefs['errorcol'] = '' +default_prefs['save_all_errors'] = True +default_prefs['savemetacol'] = '' +default_prefs['lastcheckedcol'] = '' +default_prefs['custom_cols'] = {} +default_prefs['custom_cols_newonly'] = {} +default_prefs['allow_custcol_from_ini'] = True + +default_prefs['std_cols_newonly'] = {} +default_prefs['set_author_url'] = True +default_prefs['includecomments'] = False +default_prefs['anth_comments_newonly'] = True + +default_prefs['imapserver'] = '' +default_prefs['imapuser'] = '' +default_prefs['imappass'] = '' +default_prefs['imapsessionpass'] = False +default_prefs['imapfolder'] = 'INBOX' +default_prefs['imapmarkread'] = True +default_prefs['auto_reject_from_email'] = False +default_prefs['update_existing_only_from_email'] = False +default_prefs['download_from_email_immediately'] = False + +def set_library_config(library_config,db): + db.prefs.set_namespaced(PREFS_NAMESPACE, + PREFS_KEY_SETTINGS, + library_config) + +def get_library_config(db): + library_id = get_library_uuid(db) + library_config = None + + if library_config is None: + #print("get prefs from db") + library_config = db.prefs.get_namespaced(PREFS_NAMESPACE, + PREFS_KEY_SETTINGS) + + # if don't have any settings for FanFicFarePlugin, copy from + # predecessor FanFictionDownLoaderPlugin. + if library_config is None: + logger.info("Attempting to read settings from predecessor--FFDL") + library_config = db.prefs.get_namespaced(FFDL_PREFS_NAMESPACE, + PREFS_KEY_SETTINGS) + if library_config is None: + # defaults. + logger.info("Using default settings") + library_config = copy.deepcopy(default_prefs) + + return library_config + +# 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 _get_db(self): + if self.passed_db: + return self.passed_db + else: + # In the GUI plugin we want current db so we detect when + # it's changed. CLI plugin calls need to pass db in. + return get_gui().current_db + + def __init__(self,passed_db=None): + self.default_prefs = default_prefs + self.libraryid = None + self.current_prefs = None + self.passed_db=passed_db + + def _get_prefs(self): + libraryid = get_library_uuid(self._get_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 + self.current_prefs = get_library_config(self._get_db()) + return self.current_prefs + + def __getitem__(self,k): + prefs = self._get_prefs() + if k not in prefs: + # pulls from default_prefs.defaults automatically if not set + # in default_prefs + return self.default_prefs[k] + return prefs[k] + + def __setitem__(self,k,v): + prefs = self._get_prefs() + prefs[k]=v + # self._save_prefs(prefs) + + def __delitem__(self,k): + prefs = self._get_prefs() + if k in prefs: + del prefs[k] + + def save_to_db(self): + self['last_saved_version'] = plugin_version + set_library_config(self._get_prefs(),self._get_db()) + +prefs = PrefsFacade() + diff --git a/fanficfare/adapters/adapter_archiveskyehawkecom.py b/fanficfare/adapters/adapter_archiveskyehawkecom.py index 9afb492b..d4d1732c 100644 --- a/fanficfare/adapters/adapter_archiveskyehawkecom.py +++ b/fanficfare/adapters/adapter_archiveskyehawkecom.py @@ -102,20 +102,20 @@ class ArchiveSkyeHawkeComAdapter(BaseSiteAdapter): self.story.setMetadata('authorId',author['href'].split('=')[1]) self.story.setMetadata('authorUrl','http://'+self.host+'/'+author['href']) self.story.setMetadata('author',author.string) - + authorSoup = self.make_soup(self._fetchUrl(self.story.getMetadata('authorUrl'))) - + chapter=soup.find('select',{'name':'chapter'}).findAll('option') - + for i in range(1,len(chapter)): ch=chapter[i] self.chapterUrls.append((stripHTML(ch),ch['value'])) - + self.story.setMetadata('numChapters',len(self.chapterUrls)) # eFiction sites don't help us out a lot with their meta data # formating, so it's a little ugly. - + box=soup.find('div', {'class': "container borderridge"}) sum=box.find('span').text self.setDescription(url,sum) @@ -123,7 +123,7 @@ class ArchiveSkyeHawkeComAdapter(BaseSiteAdapter): boxes=soup.findAll('div', {'class': "container bordersolid"}) for box in boxes: if box.find('b') != None and box.find('b').text == "History and Story Information": - + for b in box.findAll('b'): if "words" in b.nextSibling: self.story.setMetadata('numWords', b.text) @@ -133,29 +133,29 @@ class ArchiveSkyeHawkeComAdapter(BaseSiteAdapter): self.story.setMetadata('dateUpdated', makeDate(stripHTML(b.text), self.dateformat)) if "fandom" in b.nextSibling: self.story.addToList('category', b.text) - + for br in box.findAll('br'): br.replaceWith('split') genre=box.text.split("Genre:")[1].split("split")[0] if not "Unspecified" in genre: self.story.addToList('genre',genre) - - + + if box.find('span') != None and box.find('span').text == "WARNING": - + rating=box.findAll('span')[1] rating.find('br').replaceWith('split') rating=rating.text.replace("This story is rated",'').split('split')[0] self.story.setMetadata('rating',rating) logger.debug(self.story.getMetadata('rating')) - + warnings=box.find('ol') if warnings != None: warnings=warnings.text.replace(']', '').replace('[', '').split(' ') for warning in warnings: self.story.addToList('warnings',warning) - - + + for asoup in authorSoup.findAll('div', {'class':"story bordersolid"}): if asoup.find('a')['href'] == 'story.php?no='+self.story.getMetadata('storyId'): if '[ Completed ]' in asoup.text: @@ -167,8 +167,8 @@ class ArchiveSkyeHawkeComAdapter(BaseSiteAdapter): if not "None" in char: self.story.addToList('characters',char) break - - + + # grab the text for an individual chapter. def getChapterText(self, url): diff --git a/fanficfare/adapters/adapter_csiforensicscom.py b/fanficfare/adapters/adapter_csiforensicscom.py index 33a50e24..046f8905 100644 --- a/fanficfare/adapters/adapter_csiforensicscom.py +++ b/fanficfare/adapters/adapter_csiforensicscom.py @@ -92,7 +92,7 @@ class CSIForensicsComAdapter(BaseSiteAdapter): raise exceptions.StoryDoesNotExist(self.url) else: raise e - + # The actual text that is used to announce you need to be an # adult varies from site to site. Again, print data before # the title search to troubleshoot. @@ -166,12 +166,12 @@ class CSIForensicsComAdapter(BaseSiteAdapter): warnings = smalldiv.findAll('a',href=re.compile(r'browse.php\?type=class(&)type_id=2(&)classid=\d+')) for warning in warnings: self.story.addToList('warnings',warning.string) - + date=soup.find('div',{'class' : 'bottom'}) pd=date.find(text=re.compile("Published:")).string.split(': ') self.story.setMetadata('datePublished', makeDate(stripHTML(pd[1].split(' U')[0]), self.dateformat)) self.story.setMetadata('dateUpdated', makeDate(stripHTML(pd[2]), self.dateformat)) - + # Rated: NC-17
etc labels = soup.findAll('span',{'class':'label'}) pub=0 @@ -229,4 +229,4 @@ class CSIForensicsComAdapter(BaseSiteAdapter): raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) return self.utf8FromSoup(url,div) - + diff --git a/fanficfare/adapters/adapter_efpfanficnet.py b/fanficfare/adapters/adapter_efpfanficnet.py index dea17879..a93eb69f 100644 --- a/fanficfare/adapters/adapter_efpfanficnet.py +++ b/fanficfare/adapters/adapter_efpfanficnet.py @@ -142,10 +142,10 @@ class EFPFanFicNet(BaseSiteAdapter): # Find the chapter selector select = soup.find('select', { 'name' : 'sid' } ) - + if select is None: - # no selector found, so it's a one-chapter story. - self.chapterUrls.append((self.story.getMetadata('title'),url)) + # no selector found, so it's a one-chapter story. + self.chapterUrls.append((self.story.getMetadata('title'),url)) else: allOptions = select.findAll('option', {'value' : re.compile(r'viewstory')}) for o in allOptions: diff --git a/fanficfare/adapters/adapter_fanfictionnet.py b/fanficfare/adapters/adapter_fanfictionnet.py index 6f46893b..ae28de26 100644 --- a/fanficfare/adapters/adapter_fanfictionnet.py +++ b/fanficfare/adapters/adapter_fanfictionnet.py @@ -329,8 +329,8 @@ class FanFictionNetSiteAdapter(BaseSiteAdapter): select = soup.find('select', { 'name' : 'chapter' } ) if select is None: - # no selector found, so it's a one-chapter story. - self.chapterUrls.append((self.story.getMetadata('title'),url)) + # no selector found, so it's a one-chapter story. + self.chapterUrls.append((self.story.getMetadata('title'),url)) else: allOptions = select.findAll('option') for o in allOptions: diff --git a/fanficfare/adapters/adapter_ficbooknet.py b/fanficfare/adapters/adapter_ficbooknet.py index 16de87da..72cc1f14 100644 --- a/fanficfare/adapters/adapter_ficbooknet.py +++ b/fanficfare/adapters/adapter_ficbooknet.py @@ -82,7 +82,7 @@ class FicBookNetAdapter(BaseSiteAdapter): raise exceptions.StoryDoesNotExist(self.url) else: raise e - + # use BeautifulSoup HTML parser to make everything easier to find. soup = self.make_soup(data) @@ -93,9 +93,9 @@ class FicBookNetAdapter(BaseSiteAdapter): adult_div.extract() else: raise exceptions.AdultCheckRequired(self.url) - + # Now go hunting for all the meta data and the chapter list. - + ## Title a = soup.find('section',{'class':'chapter-info'}).find('h1') # kill '+' marks if present. @@ -180,7 +180,7 @@ class FicBookNetAdapter(BaseSiteAdapter): dlinfo = soup.find('dl',{'class':'info'}) - + i=0 fandoms = dlinfo.find('dd').findAll('a', href=re.compile(r'/fanfiction/\w+')) for fandom in fandoms: @@ -194,7 +194,7 @@ class FicBookNetAdapter(BaseSiteAdapter): ratingdt = dlinfo.find('dt',text='Рейтинг:') self.story.setMetadata('rating', stripHTML(ratingdt.next_sibling)) - + # meta=table.findAll('a', href=re.compile(r'/ratings/')) # i=0 # for m in meta: @@ -206,13 +206,13 @@ class FicBookNetAdapter(BaseSiteAdapter): # i=2 # self.story.addToList('genre', m.find('b').text) # elif i == 2: - # self.story.addToList('warnings', m.find('b').text) + # self.story.addToList('warnings', m.find('b').text) if dlinfo.find('span', {'style' : 'color: green'}): self.story.setMetadata('status', 'Completed') else: self.story.setMetadata('status', 'In-Progress') - + tags = dlinfo.findAll('dt') for tag in tags: @@ -222,7 +222,7 @@ class FicBookNetAdapter(BaseSiteAdapter): for char in chars: self.story.addToList('characters',char) break - + summary=soup.find('div', {'class' : 'urlize'}) self.setDescription(url,summary) #self.story.setMetadata('description', summary.text) diff --git a/fanficfare/adapters/adapter_fictionalleyorg.py b/fanficfare/adapters/adapter_fictionalleyorg.py index 947bf6fc..63fd870b 100644 --- a/fanficfare/adapters/adapter_fictionalleyorg.py +++ b/fanficfare/adapters/adapter_fictionalleyorg.py @@ -194,13 +194,13 @@ class FictionAlleyOrgSiteAdapter(BaseSiteAdapter): logger.debug('Getting chapter text from: %s' % url) data = self._fetchUrl(url) - # find & and - # replaced with matching div pair for easier parsing. - # Yes, it's an evil kludge, but what can ya do? Using - # something other than div prevents soup from pairing - # our div with poor html inside the story text. + # find & and + # replaced with matching div pair for easier parsing. + # Yes, it's an evil kludge, but what can ya do? Using + # something other than div prevents soup from pairing + # our div with poor html inside the story text. crazy = "crazytagstringnobodywouldstumbleonaccidently" - data = data.replace('','<'+crazy+' id="storytext">').replace('','') + data = data.replace('','<'+crazy+' id="storytext">').replace('','') # problems with some stories confusing Soup. This is a nasty # hack, but it works. diff --git a/fanficfare/adapters/adapter_phoenixsongnet.py b/fanficfare/adapters/adapter_phoenixsongnet.py index ad172c3d..e6a9fb57 100644 --- a/fanficfare/adapters/adapter_phoenixsongnet.py +++ b/fanficfare/adapters/adapter_phoenixsongnet.py @@ -76,7 +76,7 @@ class PhoenixSongNetAdapter(BaseSiteAdapter): def performLogin(self, url): params = {} - + if self.password: params['txtusername'] = self.username params['txtpassword'] = self.password @@ -168,10 +168,10 @@ class PhoenixSongNetAdapter(BaseSiteAdapter): date = b.nextSibling.string.split(': ')[1].split(',') self.story.setMetadata('dateUpdated', makeDate(date[0]+date[1], self.dateformat)) i = i+1 - - self.story.setMetadata('numChapters',len(self.chapterUrls)) - + + self.story.setMetadata('numChapters',len(self.chapterUrls)) + asoup = self.make_soup(self._fetchUrl(self.story.getMetadata('authorUrl'))) info = asoup.find('a', href=re.compile(r'fanfiction/story/'+self.story.getMetadata('storyId')+"/$")) @@ -182,13 +182,13 @@ class PhoenixSongNetAdapter(BaseSiteAdapter): if 'Rating' in b.string: self.story.setMetadata('rating', val.string.split(': ')[1]) - + if 'Words' in b.string: self.story.setMetadata('numWords', val.string.split(': ')[1]) - + if 'Setting' in b.string: self.story.addToList('category', val.string.split(': ')[1]) - + if 'Status' in b.string: if 'Completed' in val: val = 'Completed' @@ -201,9 +201,9 @@ class PhoenixSongNetAdapter(BaseSiteAdapter): info.find('br').extract() self.setDescription(url,info) break - - - # grab the text for an individual chapter. + + + # grab the text for an individual chapter. def getChapterText(self, url): logger.debug('Getting chapter text from: %s' % url) @@ -215,7 +215,7 @@ class PhoenixSongNetAdapter(BaseSiteAdapter): if "This is for problems with the formatting or the layout of the chapter." in stripHTML(p): break chapter.append(p) - + for a in chapter.findAll('div'): a.extract() for a in chapter.findAll('table'): diff --git a/fanficfare/adapters/adapter_sugarquillnet.py b/fanficfare/adapters/adapter_sugarquillnet.py index a69c6114..e5237b29 100644 --- a/fanficfare/adapters/adapter_sugarquillnet.py +++ b/fanficfare/adapters/adapter_sugarquillnet.py @@ -122,7 +122,7 @@ class SugarQuillNetAdapter(BaseSiteAdapter): self.story.setMetadata('numChapters',len(self.chapterUrls)) - ## This site doesn't have much metadata, so we will get what we can. + ## This site doesn't have much metadata, so we will get what we can. ## The metadata is all on the author's page, so we have to get it to parse. author_Url = self.story.getMetadata('authorUrl').replace('&','&') logger.debug('Getting the author page: {0}'.format(author_Url)) diff --git a/fanficfare/adapters/adapter_tthfanficorg.py b/fanficfare/adapters/adapter_tthfanficorg.py index 05d77a01..f81bd818 100644 --- a/fanficfare/adapters/adapter_tthfanficorg.py +++ b/fanficfare/adapters/adapter_tthfanficorg.py @@ -248,8 +248,8 @@ class TwistingTheHellmouthSiteAdapter(BaseSiteAdapter): select = soup.find('select', { 'name' : 'chapnav' } ) if select is None: - # no selector found, so it's a one-chapter story. - self.chapterUrls.append((self.story.getMetadata('title'),url)) + # no selector found, so it's a one-chapter story. + self.chapterUrls.append((self.story.getMetadata('title'),url)) else: allOptions = select.findAll('option') for o in allOptions: diff --git a/fanficfare/adapters/adapter_walkingtheplankorg.py b/fanficfare/adapters/adapter_walkingtheplankorg.py index e294b2a3..56d6efa0 100644 --- a/fanficfare/adapters/adapter_walkingtheplankorg.py +++ b/fanficfare/adapters/adapter_walkingtheplankorg.py @@ -135,7 +135,7 @@ class WalkingThePlankOrgAdapter(BaseSiteAdapter): return d[k] except: return "" - + labels = soup.findAll('span',{'class':'label'}) for labelspan in labels: value = labelspan.nextSibling @@ -158,7 +158,7 @@ class WalkingThePlankOrgAdapter(BaseSiteAdapter): if 'Read' in label: self.story.setMetadata('reads', value) - + if 'Categories' in label: cats = labelspan.parent.findAll('a',href=re.compile(r'browse.php\?type=categories')) catstext = [cat.string for cat in cats] diff --git a/fanficfare/adapters/adapter_whoficcom.py b/fanficfare/adapters/adapter_whoficcom.py index 3a83e0ff..06c27a7d 100644 --- a/fanficfare/adapters/adapter_whoficcom.py +++ b/fanficfare/adapters/adapter_whoficcom.py @@ -80,18 +80,18 @@ class WhoficComSiteAdapter(BaseSiteAdapter): # Find the chapter selector select = soup.find('select', { 'name' : 'chapter' } ) - + if select is None: - # no selector found, so it's a one-chapter story. - self.chapterUrls.append((self.story.getMetadata('title'),url)) + # no selector found, so it's a one-chapter story. + self.chapterUrls.append((self.story.getMetadata('title'),url)) else: - allOptions = select.findAll('option') - for o in allOptions: - url = self.url + "&chapter=%s" % o['value'] - # just in case there's tags, like in chapter titles. - title = "%s" % o - title = re.sub(r'<[^>]+>','',title) - self.chapterUrls.append((title,url)) + allOptions = select.findAll('option') + for o in allOptions: + url = self.url + "&chapter=%s" % o['value'] + # just in case there's tags, like in chapter titles. + title = "%s" % o + title = re.sub(r'<[^>]+>','',title) + self.chapterUrls.append((title,url)) self.story.setMetadata('numChapters',len(self.chapterUrls)) @@ -119,17 +119,17 @@ class WhoficComSiteAdapter(BaseSiteAdapter): a = soup.find('a', href=re.compile(r'reviews.php\?sid='+self.story.getMetadata('storyId'))) metadata = a.findParent('td') metadatachunks = self.utf8FromSoup(None,metadata,allow_replace_br_with_p=False).split('
') - + # Some stories have a
inside the description, which - # causes the number of metadatachunks to be 7 or 8 or 10 instead of 5. - # so we have to process through the metadatachunks to get the description, + # causes the number of metadatachunks to be 7 or 8 or 10 instead of 5. + # so we have to process through the metadatachunks to get the description, # then the next metadata chunk [GComyn] # process metadata for this story. description = metadatachunks[1] for i, mdc in enumerate(metadatachunks): if i==0 or i==1: - # 0 is the title section, and 1 is always the description, + # 0 is the title section, and 1 is always the description, # which is already set, so skip them [GComyn] pass else: diff --git a/fanficfare/adapters/adapter_wraithbaitcom.py b/fanficfare/adapters/adapter_wraithbaitcom.py index 6884e22c..486bc154 100644 --- a/fanficfare/adapters/adapter_wraithbaitcom.py +++ b/fanficfare/adapters/adapter_wraithbaitcom.py @@ -92,7 +92,7 @@ class WraithBaitComAdapter(BaseSiteAdapter): raise exceptions.StoryDoesNotExist(self.url) else: raise e - + if "for adults only" in data: raise exceptions.AdultCheckRequired(self.url) @@ -146,32 +146,32 @@ class WraithBaitComAdapter(BaseSiteAdapter): return d[k] except: return "" - + info = soup.find('div', {'class' : 'small'}) - + word=info.find(text=re.compile("Word count:")).split(':') self.story.setMetadata('numWords', word[1]) - + cats = info.findAll('a',href=re.compile(r'browse.php\?type=categories&id=\d')) for cat in cats: if "General" != cat.string: self.story.addToList('category',cat.string) - + chars = info.findAll('a',href=re.compile(r'browse.php\?type=characters&charid=\d')) for char in chars: self.story.addToList('characters',char.string) - + completed=info.find(text=re.compile("Completed: Yes")) if completed != None: self.story.setMetadata('status', 'Completed') else: self.story.setMetadata('status', 'In-Progress') - + date=soup.find('div',{'class' : 'bottom'}) pd=date.find(text=re.compile("Published:")).string.split(': ') self.story.setMetadata('datePublished', makeDate(stripHTML(pd[1].split(' U')[0]), self.dateformat)) self.story.setMetadata('dateUpdated', makeDate(stripHTML(pd[2]), self.dateformat)) - + # Rated: NC-17
etc labels = soup.findAll('span',{'class':'label'}) pub=0 @@ -209,7 +209,7 @@ class WraithBaitComAdapter(BaseSiteAdapter): except: # I find it hard to care if the series parsing fails pass - + info.extract() summary = soup.find('div', {'class' : 'content'}) self.setDescription(url,summary) diff --git a/fanficfare/adapters/adapter_writingwhimsicalwanderingsnet.py b/fanficfare/adapters/adapter_writingwhimsicalwanderingsnet.py index 5fbc159e..fb5f2457 100644 --- a/fanficfare/adapters/adapter_writingwhimsicalwanderingsnet.py +++ b/fanficfare/adapters/adapter_writingwhimsicalwanderingsnet.py @@ -81,7 +81,7 @@ class WritingWhimsicalwanderingsNetAdapter(BaseSiteAdapter): addurl = '&ageconsent=ok&warning=4' else: addurl= '' - + try: data = self._fetchUrl(url+addurl) except urllib2.HTTPError, e: @@ -115,10 +115,10 @@ class WritingWhimsicalwanderingsNetAdapter(BaseSiteAdapter): self.story.setMetadata('numChapters',len(self.chapterUrls)) - ## This site's metadata is not very well formatted... so we have to cludge a bit.. - ## The only ones I see that are, are Relationships and Warnings... + ## This site's metadata is not very well formatted... so we have to cludge a bit.. + ## The only ones I see that are, are Relationships and Warnings... ## However, the categories, characters, and warnings are all links, so we can get them easier - + ## Categories don't have a proper label, but do use links, so... cats = soup.findAll('a',href=re.compile(r'browse.php\?type=categories')) catstext = [cat.string for cat in cats] @@ -137,7 +137,7 @@ class WritingWhimsicalwanderingsNetAdapter(BaseSiteAdapter): for warning in warningstext: if warning != None: self.story.addToList('warnings',warning.string) - + ## Relationships do have a proper label, but we will use links anyway ## this is actually tag information ... m/f, gen, m/m and such. ## so I'm putting them in the extratags section. @@ -154,7 +154,7 @@ class WritingWhimsicalwanderingsNetAdapter(BaseSiteAdapter): while '||||||||' in metad: metad = metad.replace('||||||||','|||||||') metad = stripHTML(metad) - + for mdata in metad.split('|||||||'): mdata = mdata.strip() if mdata.startswith('Summary:'): @@ -200,7 +200,7 @@ class WritingWhimsicalwanderingsNetAdapter(BaseSiteAdapter): except: self.setSeries(series_name,0) pass - + storynotes = soup.find('blockquote') if storynotes != None: storynotes = stripHTML(storynotes).replace('Story Notes:','') diff --git a/fanficfare/configurable.py b/fanficfare/configurable.py index 9aa1866e..b5e85c45 100644 --- a/fanficfare/configurable.py +++ b/fanficfare/configurable.py @@ -199,7 +199,7 @@ def get_valid_set_options(): 'fix_fimf_blockquotes':(['fimfiction.net'],None,boollist), 'fail_on_password':(['fimfiction.net'],None,boollist), 'keep_prequel_in_description':(['fimfiction.net'],None,boollist), - 'include_author_notes':(['fimfiction.net'],None,boollist), + 'include_author_notes':(['fimfiction.net'],None,boollist), 'do_update_hook':(['fimfiction.net', 'archiveofourown.org'],None,boollist), 'always_login':(['archiveofourown.org'],None,boollist), @@ -354,7 +354,7 @@ def get_valid_keywords(): 'find_chapters', 'fix_fimf_blockquotes', 'keep_prequel_in_description', - 'include_author_notes', + 'include_author_notes', 'force_login', 'generate_cover_settings', 'grayscale_images', diff --git a/fanficfare/writers/writer_epub.py b/fanficfare/writers/writer_epub.py index 8db5d83c..a94b8f38 100644 --- a/fanficfare/writers/writer_epub.py +++ b/fanficfare/writers/writer_epub.py @@ -735,7 +735,7 @@ div { margin: 0pt; padding: 0pt; } if self.story.calibrebookmark: outputepub.writestr("META-INF/calibre_bookmarks.txt",self.story.calibrebookmark) - # declares all the files created by Windows. otherwise, when + # declares all the files created by Windows. otherwise, when # it runs in appengine, windows unzips the files as 000 perms. for zf in outputepub.filelist: zf.create_system = 0 diff --git a/makeplugin.py b/makeplugin.py index 858c79e5..62b61da7 100644 --- a/makeplugin.py +++ b/makeplugin.py @@ -1,53 +1,53 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright 2015, Jim Miller - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -from glob import glob - -from makezip import createZipFile - -if __name__=="__main__": - filename="FanFicFare.zip" - exclude=['*.pyc','*~','*.xcf','*[0-9].png','*.po','*.pot','*default.mo','*Thumbs.db'] - - os.chdir('calibre-plugin') - files=['plugin-defaults.ini','plugin-example.ini','about.html', - 'images','translations'] - files.extend(glob('*.py')) - files.extend(glob('plugin-import-name-*.txt')) - # 'w' for overwrite - createZipFile("../"+filename,"w", - files, - exclude=exclude) - - os.chdir('../included_dependencies') - files=['gif.py','bs4','chardet','html2text'] - # calibre has it's own copies of these that precedence anyway: - # 'six.py','html5lib','webencodings' - # webencodings is only needed by versions of html5lib after 0.9x7 - # 'a' for append - createZipFile("../"+filename,"a", - files, - exclude=exclude) - - os.chdir('..') - # 'a' for append - files=['fanficfare'] - createZipFile(filename,"a", - files, - exclude=exclude) - +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright 2015, Jim Miller + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +from glob import glob + +from makezip import createZipFile + +if __name__=="__main__": + filename="FanFicFare.zip" + exclude=['*.pyc','*~','*.xcf','*[0-9].png','*.po','*.pot','*default.mo','*Thumbs.db'] + + os.chdir('calibre-plugin') + files=['plugin-defaults.ini','plugin-example.ini','about.html', + 'images','translations'] + files.extend(glob('*.py')) + files.extend(glob('plugin-import-name-*.txt')) + # 'w' for overwrite + createZipFile("../"+filename,"w", + files, + exclude=exclude) + + os.chdir('../included_dependencies') + files=['gif.py','bs4','chardet','html2text'] + # calibre has it's own copies of these that precedence anyway: + # 'six.py','html5lib','webencodings' + # webencodings is only needed by versions of html5lib after 0.9x7 + # 'a' for append + createZipFile("../"+filename,"a", + files, + exclude=exclude) + + os.chdir('..') + # 'a' for append + files=['fanficfare'] + createZipFile(filename,"a", + files, + exclude=exclude) + diff --git a/makezip.py b/makezip.py index c7016914..0028a029 100644 --- a/makezip.py +++ b/makezip.py @@ -1,52 +1,52 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright 2015, Jim Miller - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os, zipfile, sys -from glob import glob - -def addFolderToZip(myZipFile,folder,exclude=[]): - folder = folder.encode('ascii') #convert path to ascii for ZipFile Method - excludelist=[] - for ex in exclude: - excludelist.extend(glob(folder+"/"+ex)) - for file in glob(folder+"/*"): - if file in excludelist: - continue - if os.path.isfile(file): - #print file - myZipFile.write(file, file, zipfile.ZIP_DEFLATED) - elif os.path.isdir(file): - addFolderToZip(myZipFile,file,exclude=exclude) - -def createZipFile(filename,mode,files,exclude=[]): - myZipFile = zipfile.ZipFile( filename, mode ) # Open the zip file for writing - excludelist=[] - for ex in exclude: - excludelist.extend(glob(ex)) - for file in files: - if file in excludelist: - continue - file = file.encode('ascii') #convert path to ascii for ZipFile Method - if os.path.isfile(file): - (filepath, filename) = os.path.split(file) - #print file - myZipFile.write( file, filename, zipfile.ZIP_DEFLATED ) - if os.path.isdir(file): - addFolderToZip(myZipFile,file,exclude=exclude) - myZipFile.close() - return (1,filename) - +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright 2015, Jim Miller + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os, zipfile, sys +from glob import glob + +def addFolderToZip(myZipFile,folder,exclude=[]): + folder = folder.encode('ascii') #convert path to ascii for ZipFile Method + excludelist=[] + for ex in exclude: + excludelist.extend(glob(folder+"/"+ex)) + for file in glob(folder+"/*"): + if file in excludelist: + continue + if os.path.isfile(file): + #print file + myZipFile.write(file, file, zipfile.ZIP_DEFLATED) + elif os.path.isdir(file): + addFolderToZip(myZipFile,file,exclude=exclude) + +def createZipFile(filename,mode,files,exclude=[]): + myZipFile = zipfile.ZipFile( filename, mode ) # Open the zip file for writing + excludelist=[] + for ex in exclude: + excludelist.extend(glob(ex)) + for file in files: + if file in excludelist: + continue + file = file.encode('ascii') #convert path to ascii for ZipFile Method + if os.path.isfile(file): + (filepath, filename) = os.path.split(file) + #print file + myZipFile.write( file, filename, zipfile.ZIP_DEFLATED ) + if os.path.isdir(file): + addFolderToZip(myZipFile,file,exclude=exclude) + myZipFile.close() + return (1,filename) + diff --git a/webservice/ffstorage.py b/webservice/ffstorage.py index d41a2455..3452faed 100644 --- a/webservice/ffstorage.py +++ b/webservice/ffstorage.py @@ -17,49 +17,48 @@ import pickle, copy from google.appengine.ext import db class ObjectProperty(db.Property): - data_type = db.Blob + data_type = db.Blob - def get_value_for_datastore(self, model_instance): - value = self.__get__(model_instance, model_instance.__class__) - pickled_val = pickle.dumps(value,protocol=pickle.HIGHEST_PROTOCOL) - if value is not None: return db.Blob(pickled_val) + def get_value_for_datastore(self, model_instance): + value = self.__get__(model_instance, model_instance.__class__) + pickled_val = pickle.dumps(value,protocol=pickle.HIGHEST_PROTOCOL) + if value is not None: return db.Blob(pickled_val) - def make_value_from_datastore(self, value): - if value is not None: return pickle.loads(value) + def make_value_from_datastore(self, value): + if value is not None: return pickle.loads(value) - def default_value(self): - return copy.copy(self.default) + def default_value(self): + return copy.copy(self.default) class DownloadMeta(db.Model): - user = db.UserProperty() - url = db.StringProperty() - name = db.StringProperty() - title = db.StringProperty() - author = db.StringProperty() - format = db.StringProperty() - failure = db.TextProperty() - completed = db.BooleanProperty(default=False) - date = db.DateTimeProperty(auto_now_add=True) - version = db.StringProperty() - ch_begin = db.StringProperty() - ch_end = db.StringProperty() - # data_chunks is implicit from DownloadData def. + user = db.UserProperty() + url = db.StringProperty() + name = db.StringProperty() + title = db.StringProperty() + author = db.StringProperty() + format = db.StringProperty() + failure = db.TextProperty() + completed = db.BooleanProperty(default=False) + date = db.DateTimeProperty(auto_now_add=True) + version = db.StringProperty() + ch_begin = db.StringProperty() + ch_end = db.StringProperty() + # data_chunks is implicit from DownloadData def. class DownloadData(db.Model): - download = db.ReferenceProperty(DownloadMeta, - collection_name='data_chunks') - blob = db.BlobProperty() - index = db.IntegerProperty() + download = db.ReferenceProperty(DownloadMeta, + collection_name='data_chunks') + blob = db.BlobProperty() + index = db.IntegerProperty() class UserConfig(db.Model): - user = db.UserProperty() - config = db.BlobProperty() + user = db.UserProperty() + config = db.BlobProperty() class SavedMeta(db.Model): - url = db.StringProperty() - title = db.StringProperty() - author = db.StringProperty() - date = db.DateTimeProperty(auto_now_add=True) - count = db.IntegerProperty() - meta = ObjectProperty() - + url = db.StringProperty() + title = db.StringProperty() + author = db.StringProperty() + date = db.DateTimeProperty(auto_now_add=True) + count = db.IntegerProperty() + meta = ObjectProperty()