PK 2AQc-K K config.py# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai from __future__ import absolute_import __license__ = 'GPL v3' __copyright__ = '2011-2018, meme' __docformat__ = 'restructuredtext en' ##################################################################### # Configuration data read/write/validation routines ##################################################################### import os, re from functools import partial try: from PyQt5 import Qt as QtGui from PyQt5.Qt import Qt, QWidget, QVBoxLayout, QHBoxLayout, QLabel, \ QLineEdit, QFormLayout, QTableWidget, QTableWidgetItem, \ QAbstractItemView, QComboBox, QMenu, QToolButton, QIcon, \ QCheckBox except: from PyQt4 import QtGui from PyQt4.Qt import Qt, QWidget, QVBoxLayout, QHBoxLayout, QLabel, \ QLineEdit, QFormLayout, QTableWidget, QTableWidgetItem, \ QAbstractItemView, QComboBox, QMenu, QToolButton, QIcon, \ QCheckBox import calibre_plugins.kindle_collections.messages as msg import calibre_plugins.kindle_collections.calibre_info as calibre_info from calibre_plugins.kindle_collections.utilities import debug_print, csv_to_array from calibre_plugins.kindle_collections.__init__ import PLUGIN_NAME PLUGIN_STORE_DIR = 'plugins' # Location of plugins in the Calibre config dir PLUGIN_STORE_FILE = '%s.json' % PLUGIN_NAME # Name of the file holding the customization data STORE_VERSION = '3' # Update if changing what is stored in the customization json file STORE_KEY_ROWS = 'Rows' # Specific key to hold the configuration for the table of columns/collections STORE_KEY_SETTINGS = 'Settings' # Specific key to hold additional settings MENU_CLICK_STYLE = 'Kindle Collections: Menu Style' MENU_DEFAULT = 'Show menu (default)' MENU_CREATE = 'Create Collections' MENU_PREVIEW = 'Preview Collections' MENU_CUSTOMIZE = 'Customize plugin' MENU_VIEW = 'View report' MENU_EDIT = 'Edit collections' MENU_IMPORT = 'Import collections' MENU_MODIFY = 'Modify settings' MENU_OPTIONS = [ MENU_DEFAULT, MENU_CREATE, MENU_PREVIEW, MENU_CUSTOMIZE, MENU_VIEW, MENU_EDIT, MENU_IMPORT, MENU_MODIFY ] OPT_NONE = '' # Options for customization OPT_CREATE = 'Create' OPT_DELETE = 'Delete' ACTION_OPTIONS = [ OPT_NONE, OPT_CREATE, OPT_DELETE ] CUSTOMIZE_DEFAULTS = { # Default customization values for the table # 'tags': { # 'action': OPT_CREATE, # 'column': '', # 'prefix': '_', # 'suffix': '', # 'minimum': '1', # 'ignore': [], # 'include': [], # 'rename_from': '', # 'rename_to': '', # 'split_char': '' # } } SETTINGS_DEFAULTS = { # Default customization values for additional settings 'ignore_all': [], 'keep_kindle_only': True, 'ignore_prefix_suffix': True, 'ignore_case': True, 'reset_collection_times': True, 'fast_reboot': True, 'ignore_json_db': False, 'diff_db_only': False, 'kindle_model_version': -1, 'store_version': 'Not Yet Customized' } OLD_STORE_NAME = 'Customization' # Old key name for customization - new version uses library/device id STORE_DEFAULTS = { # Format used by customization json file to store settings STORE_KEY_ROWS: CUSTOMIZE_DEFAULTS, STORE_KEY_SETTINGS: SETTINGS_DEFAULTS } CUSTOMIZE_COLUMNS = { 'action': { 'pos': 0, 'title': 'Action', 'blank': OPT_NONE}, 'column': { 'pos': 1, 'title': 'Calibre Source', 'blank': '' }, 'prefix': { 'pos': 2, 'title': 'Prefix', 'blank': '' }, 'suffix': { 'pos': 3, 'title': 'Suffix', 'blank': '' }, 'minimum': { 'pos': 4, 'title': 'Minimum', 'blank': '' }, 'ignore': { 'pos': 5, 'title': 'Ignore names matching these patterns', 'blank': '' }, 'include': { 'pos': 6, 'title': 'Include names matching these patterns', 'blank': '' }, 'rename_from': { 'pos': 7, 'title': 'Rename these patterns...', 'blank': '' }, 'rename_to': { 'pos': 8, 'title': '...to these patterns', 'blank': '' }, 'split_char': { 'pos': 9, 'title': 'Split on character', 'blank': '' } } config_table = None # Configuration info for table config_settings = {} # Configuration info for other choices store = None ##################################################################### def init(parent): global store debug_print('BEGIN Configuration loading config store') msg.init(parent) calibre_info.init(parent) ok = True if calibre_info.ci.library_uuid != '' and calibre_info.ci.device_uuid != '': if calibre_info.device_metadata_available: store = ConfigStore(calibre_info.ci.library_uuid, calibre_info.ci.device_uuid) store_data = store.read_store_data() load_config_settings(store_data.get(STORE_KEY_SETTINGS)) load_config_table(store_data.get(STORE_KEY_ROWS)) else: msg.message.error('Calibre has not finished loading data from the Kindle.
Please wait until the Jobs indicator shows 0 jobs running.') ok = False else: msg.message.error('Kindle not detected.
This plugin requires a Kindle to be connected so that the plugin can read existing collection information and the data Calibre stored on the Kindle.') ok = False debug_print('END Configuration loading config store') return ok def load_config_settings(settings=STORE_DEFAULTS[STORE_KEY_SETTINGS]): global config_settings config_settings = settings for i in STORE_DEFAULTS[STORE_KEY_SETTINGS].keys(): if i not in config_settings: config_settings[i] = STORE_DEFAULTS[STORE_KEY_SETTINGS][i] debug_print('Setting "%s" not found in json file, loading default value "%s"' % (i,config_settings[i])) def load_config_table(table=STORE_DEFAULTS[STORE_KEY_ROWS]): global config_table config_table = table # Ensure config_table has every Calibre column - fill new ones with blank values for label in calibre_info.ci.column_labels.keys(): if label not in config_table: config_table[label] = create_blank_row_data() # Remove any saved columns that don't exist in Calibre for row in tuple(config_table.keys()): if row not in calibre_info.ci.column_labels: del config_table[row] # And make sure every value for a row exists (if extra, doesn't matter as they won't be saved/used) for row in config_table.keys(): for entry in CUSTOMIZE_COLUMNS.keys(): if entry == 'column': # Save a name for the title column in the table config_table[row]['column'] = calibre_info.ci.column_labels[row] elif entry not in config_table[row]: config_table[row][entry] = CUSTOMIZE_COLUMNS[entry]['blank'] debug_print('Adding "%s" to row "%s"' % (entry, row)) def create_blank_row_data(): data = {} for k in CUSTOMIZE_COLUMNS.keys(): data[k] = CUSTOMIZE_COLUMNS[k]['blank'] return data ########################################################################## def validate_pattern(pattern): message_text = '' valid = True if len(pattern) > 0: try: r = re.match(pattern,'') if re.match('\|', pattern): valid=False message_text = '. Cannot start with "|", try "\|"' except: valid = False return (valid, message_text) # Check entered patterns to see if they are valid - to issue just 1 error message def validate_configuration(row_data, ignore_all_pattern): debug_print('BEGIN Validating customization values') valid = True if len(row_data) == 0: valid = False msg.message.error('No table items - no Calibre data found') # Check the ignore/include fields for valid patterns for row in row_data.keys(): fields = row_data[row] for field in [ 'ignore', 'include' ]: for pattern in fields[field]: (avalid, message_text) = validate_pattern(pattern) if not avalid: valid = False msg.message.error('Invalid pattern "%s" - in row "%s", column "%s"%s.' % (pattern, cc.column_labels[row], field, message_text)) # Check the substitute columns for valid patterns rename_from = fields['rename_from'] rename_to = fields['rename_to'] rename_from_empty = (not rename_from) or rename_from.isspace() rename_to_empty = (not rename_to) or rename_to.isspace() avalid = True if len(rename_from) > 0 or len(rename_to) > 0: if rename_from_empty or rename_to_empty: avalid = False else: try: r = re.sub(rename_from, rename_to, '') except: avalid = False if avalid == False: valid = False msg.message.error('Invalid substitute patterns - from "%s", to "%s" - in row "%s".' % (rename_from, rename_to, row)) minimum = fields['minimum'] if minimum != '': if not minimum.isdigit(): m = -1 else: m = int(minimum) if m < 0: valid = False msg.message.error('Invalid number "%s" in customized row "%s", column "%s".' % (minimum, row, 'minimum')) # Check the overall ignore field for pattern in csv_to_array(ignore_all_pattern): (avalid, message_text) = validate_pattern(pattern) if not avalid: valid = False msg.message.error('Invalid pattern "%s" in ignore always field "%s"%s."' % (pattern, row, message_text)) debug_print('END Validating customization values, returning %s' % valid) return valid ##################################################################### class ConfigStore(): def __init__(self, library_uuid, device_uuid): debug_print('BEGIN Initializing ConfigStore') self.library_uuid = library_uuid self.device_uuid = device_uuid self.plugin_config_store = '' debug_print('END Initializing ConfigStore') # 1.3.X had a bug that saved config settings in the wrong directory, and 1.4.0 renamed the file def fix_legacy_store_path(self): from calibre.utils.config import config_dir debug_print('BEGIN Fixing legacy store path') OLDNAME = 'Create Kindle Collections.json' new_store_absolute_path = self.get_calibre_plugin_store_absolute_path() if not os.path.isfile(new_store_absolute_path): # If pre 1.3.X version, just rename the file old_name_absolute_path = os.path.join(config_dir, PLUGIN_STORE_DIR, OLDNAME) if os.path.isfile(old_name_absolute_path): os.rename(old_name_absolute_path, new_store_absolute_path) debug_print('Fixed legacy store file location %s -> %s' % (old_name_absolute_path, new_store_absolute_path)) incorrect_absolute_path = os.path.join(config_dir, re.sub('^.','',config_dir), PLUGIN_STORE_DIR, OLDNAME) # If 1.3.X version, move file from subdirectory (and overwrite any older version) if os.path.isfile(incorrect_absolute_path): # If pre 1.3.X version was there, just delete it and replace with newer version if os.path.isfile(new_store_absolute_path): os.remove(new_store_absolute_path) os.rename(incorrect_absolute_path, new_store_absolute_path) debug_print('Fixed legacy incorrect store file location %s -> %s' % (incorrect_absolute_path, new_store_absolute_path)) debug_print('END Fixing legacy store path') # 1.3.X and before did not use the actual field label as name, and did not have action column, different settings def fix_legacy_config_values(self, data): debug_print('BEGIN Fixing legacy config values') if data: table = data.get(STORE_KEY_ROWS) for row_index in tuple(table.keys()): if 'collect' in table[row_index]: if table[row_index]['collect']: debug_print('Fixing legacy value collect for "%s" to use new action option' % row_index) table[row_index]['action'] = OPT_CREATE else: table[row_index]['action'] = OPT_NONE del table[row_index]['collect'] if row_index not in calibre_info.ci.column_labels: custom = '#' + row_index if custom in calibre_info.ci.column_labels: debug_print('Fixing legacy label - renaming %s to %s' % (custom, row_index)) table[custom] = table[row_index] del table[row_index] settings = data.get(STORE_KEY_SETTINGS) # Version 1.2 had no settings, Version 1.3.X had different names, missing settings if settings: if 'ignore_prefix' in settings: settings['ignore_prefix_suffix'] = settings['ignore_prefix'] debug_print('Fixing legacy settings for ignore_prefix to use ignore_prefix_suffix') if 'fast_reboot' not in settings: settings['fast_reboot'] = STORE_DEFAULTS[STORE_KEY_SETTINGS]['fast_reboot'] debug_print('Fixing legacy settings to add fast_reboot option') else: settings = STORE_DEFAULTS[STORE_KEY_SETTINGS] data[STORE_KEY_ROWS] = table data[STORE_KEY_SETTINGS] = settings debug_print('END Fixing legacy config values') return data # Handle settings migration when updating the store version... def upgrade_config_values(self, data): debug_print('BEGIN Upgrading config values') if data: if STORE_KEY_SETTINGS in data: # This was superceded by the in-config device detection/selection in 1.7.11.N if 'force_touch_model' in data[STORE_KEY_SETTINGS]: debug_print('Removing deprecated setting force_touch_model') del data[STORE_KEY_SETTINGS]['force_touch_model'] # Do the stuff from 1.7.11, too, even if it's not terribly necessary, there's already a fallback in place for new settings if 'ignore_json_db' not in data[STORE_KEY_SETTINGS]: data[STORE_KEY_SETTINGS]['ignore_json_db'] = STORE_DEFAULTS[STORE_KEY_SETTINGS]['ignore_json_db'] debug_print('Fixing settings to add ignore_json_db option') if 'diff_db_only' not in data[STORE_KEY_SETTINGS]: data[STORE_KEY_SETTINGS]['diff_db_only'] = STORE_DEFAULTS[STORE_KEY_SETTINGS]['diff_db_only'] debug_print('Fixing settings to add diff_db_only option') if 'kindle_model_version' not in data[STORE_KEY_SETTINGS]: data[STORE_KEY_SETTINGS]['kindle_model_version'] = STORE_DEFAULTS[STORE_KEY_SETTINGS]['kindle_model_version'] debug_print('Fixing settings to add kindle_model_version option') debug_print('END Upgrading config values') return data def get_calibre_plugin_store_path(self): return os.path.join(PLUGIN_STORE_DIR, PLUGIN_STORE_FILE) def get_calibre_plugin_store_absolute_path(self): from calibre.utils.config import config_dir return os.path.join(config_dir, self.get_calibre_plugin_store_path()) def is_current_store_format(self): debug_print('BEGIN Is Current Store Format') ok = True if 'store_version' in config_settings: store_version = config_settings['store_version'] else: store_version = 'Unexpected error' if store_version != STORE_VERSION: ok = False debug_print('Invalid store version - found %s instead of %s' % (store_version, STORE_VERSION)) debug_print('END Is Current Store Format=%s, store_version=%s' % (ok, store_version)) return ok def fix_legacy_stuff(self): debug_print('Fixing legacy stuff') self.fix_legacy_store_path() # Legacy plugin used to store data under just one name c = self.config_store() if c: old_store_data = c.get(OLD_STORE_NAME, None) if old_store_data: debug_print('Fixing old store key to use library/Kindle uuid instead of "%s"' % OLD_STORE_NAME) new_store_data = self.fix_legacy_config_values(old_store_data) if OLD_STORE_NAME in c: del c[OLD_STORE_NAME] self.write_store_data(new_store_data) def read_store_data(self): debug_print('Reading store information') # Plugin used to store json file under a different name/path self.fix_legacy_stuff() device_data = {} c = self.config_store() if c: # Read the data: library { device: data } library_data = c.get(self.library_uuid) if library_data and self.device_uuid in library_data: device_data = library_data[self.device_uuid] if not device_data: device_data = STORE_DEFAULTS debug_print('Loaded default configuration information') else: debug_print('Loaded data from library "%s", device "%s"' % (self.library_uuid, self.device_uuid)) # Perform settings migration if need be updated_device_data = self.upgrade_config_values(device_data) return updated_device_data def write_store_data(self, device_data): debug_print('BEGIN Writing store information') c = self.config_store() library_data = c.get(self.library_uuid) if library_data: debug_print('Using existing store for this library') library_data[self.device_uuid] = device_data else: debug_print('No existing store for this library so creating it') library_data = { self.device_uuid: device_data } c.set(self.library_uuid, library_data) debug_print('END Writing store information') def config_store(self): from calibre.utils.config import JSONConfig if not self.plugin_config_store: filename = self.get_calibre_plugin_store_path() debug_print('Setting customization config_store to "%s"' % os.path.join(filename)) try: # Instantiate configuration data store from JSON file self.plugin_config_store = JSONConfig(filename) except: debug_print('Unexpected error with JSONConfig') if not self.plugin_config_store: debug_print('No customization file found, will use defaults for all values') return self.plugin_config_store PK PvL L kindle_device.py# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai from __future__ import absolute_import import six __license__ = 'GPL v3' __copyright__ = '2011-2018, meme' __docformat__ = 'restructuredtext en' ##################################################################### # Kindle Device related functions ##################################################################### import os, json, re from calibre.constants import iswindows from shutil import copyfile import calibre_plugins.kindle_collections.config as cfg from calibre_plugins.kindle_collections.utilities import debug_print PATH_CACHE_FILE = 'kindle_collections_path_cache.calibre' COLLECTION_DIR = 'system' COLLECTION_FILE = 'collections.json' IGNORE_EXTENSIONS = ['apnx', 'mbp', 'mpb1', 'mbs', 'sdr', 'mp3', 'tan', 'tas', 'tal', 'pdr', 'han', 'asc', 'azw3f', 'azw3r', 'phl', 'ea', 'eal', 'json', 'yjf', 'yjr', 'meta', 'mf', 'kll', 'previewData'] IGNORE_PATHS = ['sdr'] BOOK_DIRECTORIES = ['documents', 'audible'] READER_PREF_FILE = 'system/com.amazon.ebook.booklet.reader/reader.pref' FONT_DIR = 'fonts' kdevice = None ##################################################################### def init(device_path): global kdevice kdevice = KindleDevice(device_path) debug_print('Set kdevice') # Class for reading from/writing to the Kindle class KindleDevice(): def __init__(self, root): debug_print('BEGIN Initializing KindleDevice with %s' % root) self.collections = {} # If diff_db_only is set, we want to load the full database, not the diff if cfg.config_settings['diff_db_only']: self.full_db = True else: self.full_db = False # Check for systems directory to be sure its mounted ok, and appears to be a Kindle self.root = '' if root: try: system_dir = os.path.join(root, COLLECTION_DIR) if os.path.isdir(system_dir): self.root = root debug_print('Kindle device found') else: debug_print('Device does not seem to be a Kindle - no "%s" directory found' % system_dir) except: debug_print('Unexpected error in checking Kindle system directory: %s' % system_dir) pass self.collections = self.get_collections() debug_print('END Initializing KindleDevice') def get_fullpath(self, path): return os.path.normpath(os.path.join(self.root, path)) def get_collections(self): debug_print('BEGIN getting collections from file') if not self.collections: self.collections = self.read_json_file(self.get_collections_filename()) debug_print('END getting collections from file') return self.collections def get_compare_collections(self): collections = self.read_json_file(self.get_compare_filename()) return collections def get_collections_filename(self, override=False): # We want to be able to get the default filename sometimes, even with diff_db_only if override: return os.path.join(self.root, COLLECTION_DIR, COLLECTION_FILE) else: if self.full_db: return os.path.join(self.root, COLLECTION_DIR, COLLECTION_FILE + '.full') else: return os.path.join(self.root, COLLECTION_DIR, COLLECTION_FILE) def get_backup_filename(self): return self.get_collections_filename() + '.backup' def get_compare_filename(self): return self.get_collections_filename() + '.compare' def get_original_backup_filename(self): return self.get_collections_filename() + '.original' def get_tmp_backup_filename(self): return self.get_collections_filename() + '.tmpfile' # Return contents of a json formatted file - return None if no file/invalid data def read_json_file(self, pathname): json_list = None try: debug_print('Reading file: %s' % pathname) if os.path.isfile(pathname): with open(pathname, 'r') as f: json_list = json.load(f) else: debug_print('No such file: %s' % pathname) except: debug_print('Unexpected error, unable to read file: %s' % pathname) pass return json_list # Return relative path of an absolute path on the Kindle def absolute_path(self, lpath): return os.path.join(self.root, lpath) # Return absolute path of a relative path on the Kindle def relative_path(self, path): return os.path.relpath(path, self.root) def restore_collections(self): json_file = self.get_collections_filename() backup_json_file = self.get_backup_filename() original_backup_json_file = self.get_original_backup_filename() tmp_json_file = self.get_tmp_backup_filename() debug_print('BEGIN Restore, trying to restore Kindle collections file: %s' % json_file) if not os.path.isfile(backup_json_file): if os.path.isfile(original_backup_json_file): debug_print('Using original backup collections file %s' % original_backup_json_file) try: copyfile(original_backup_json_file, backup_json_file) except: raise ValueError('Unable to copy original backup file.') else: raise ValueError('No backup collections file to restore.') else: debug_print('Using backup collections file %s' % backup_json_file) if os.path.isfile(backup_json_file): try: # Swap backup file and current file if os.path.isfile(json_file): if os.path.isfile(tmp_json_file): os.remove(tmp_json_file) os.rename(json_file, tmp_json_file) os.rename(backup_json_file, json_file) if os.path.isfile(tmp_json_file): os.rename(tmp_json_file, backup_json_file) except any as e: raise ValueError('Unable to restore backup file. %s' % str(e)) else: # No file to restore raise ValueError('No backup collections file exists.') debug_print('END Restore') # Save collections list to Kindle file after making a backup of the existing file def save_collections(self, collections, diff_collections, compare=False): json_file = self.get_collections_filename() backup_json_file = self.get_backup_filename() original_backup_json_file = self.get_original_backup_filename() compare_json_file = self.get_compare_filename() debug_print('Trying to save collections to Kindle file: %s' % json_file) try: # Save and always keep the first json file that existed before this plugin ran if os.path.isfile(original_backup_json_file): # Rename might not work if destination exists, and we don't want to just delete backup if os.path.isfile(backup_json_file): os.remove(backup_json_file) if os.path.isfile(json_file): os.rename(json_file, backup_json_file) debug_print('Backup Kindle collections file created: %s' % backup_json_file) else: if os.path.isfile(json_file): os.rename(json_file, original_backup_json_file) debug_print('Original backup Kindle collections file created: %s' % original_backup_json_file) except: raise ValueError('No collections modified - unable to backup existing collection file: %s' % json_file) else: if not collections or len(collections) < 1: # No collections - delete the file try: if os.path.isfile(json_file): os.remove(json_file) except: raise ValueError('Collections were empty - but unable to remove existing collection file') else: try: cf = open(json_file, 'w') except: raise ValueError('No collections modified - unable to open collections file for writing: %s' % json_file) else: try: json.dump(collections, cf) cf.close debug_print('Saved collections file') except: # Restore the backup of the file since we were unable to save the new file debug_print('\nRestoring backup Kindle collections file since new file could not be modified: %s' % backup_json_file) try: if os.path.isfile(json_file): os.remove(json_file) if os.path.isfile(backup_json_file): os.rename(backup_json_file, json_file) raise ValueError('No collections modified. Unable to save data to collections file. Restored original file from backup: %s' % json_file) except: raise ValueError('Problem saving collections file. Unable to restore backup file: %s' % backup_json_file) # If we asked for diff_db_only, save it with the default filename if self.full_db: diff_file = self.get_collections_filename(override=True) debug_print('Trying to save updated collections to Kindle file: %s' % diff_file) # KISS, don't remove the db if there's no changes, or try to restore a backup in case of save failure, that's counter-intuitive with what we're tryong to do in diff_db_only mode... if not diff_collections or len(diff_collections) < 1: # If we don't even have a collection file to import (first use?), use the full one! if not os.path.isfile(diff_file): debug_print('No updated collections, we\'re in diff_db_only mode, but we don\'t have a collection file (first run?). Use the full database instead.') try: cf = open(diff_file, 'w') except: raise ValueError('No collections modified - unable to open collections file for writing: %s' % diff_file) else: try: json.dump(collections, cf) cf.close debug_print('Saved collections file') except: debug_print('\nNew collection file could not me modified, but we\'re in diff_db_only mode, so don\'t restore a backup Kindle collections file.') raise ValueError('Unable to save full collections to file.') else: # No collections - don't do a thing debug_print('No updated collections, but we\'re in diff_db_only mode, so don\'t do a thing.') else: try: cf = open(diff_file, 'w') except: raise ValueError('No collections modified - unable to open collections file for writing: %s' % diff_file) else: try: json.dump(diff_collections, cf) cf.close debug_print('Saved collections file') except: # Don't restore the backup of the file since we were unable to save the new file. debug_print('\nNew collection file could not me modified, but we\'re in diff_db_only mode, so don\'t restore a backup Kindle collections file.') raise ValueError('Problem saving collections file.') # Copy the current file if this is Create Collections for later comparison if compare and os.path.isfile(json_file): try: f = open(compare_json_file, 'w') json.dump(collections, f) f.close debug_print('Saved copy of collections for later comparison') except: raise ValueError('Unable to copy json file for later comparison') def get_file_paths(self): from calibre.ebooks.metadata.meta import path_to_ext debug_print('BEGIN getting file paths on Kindle') self.paths = [] prefix_length = len(self.root) for ebook_dir in BOOK_DIRECTORIES: ebook_dir = six.text_type(os.path.join(self.root, ebook_dir)) debug_print('Checking Kindle directory for books: %s' % ebook_dir) if not os.path.exists(ebook_dir): debug_print('Directory does not exist on Kindle: %s' % ebook_dir) continue for path, dirs, files in os.walk(ebook_dir, topdown=True): # Completely ignore the sidecar folder (and all its subfolders) on the Touch/PaperWhite to avoid doing useless IO dirs[:] = [d for d in dirs if path_to_ext(d) not in IGNORE_PATHS] file_count = len(files) if files else 0 dir_count = len(dirs) if dirs else 0 debug_print('Checking path: %s, %d files, %d directories' % (path, file_count, dir_count)) for filename in files: if not path_to_ext(filename) in IGNORE_EXTENSIONS: fullpath = os.path.normpath(os.path.join(path, filename)) if iswindows: fullpath = fullpath.replace('\\', '/') self.paths.append(fullpath[prefix_length:]) debug_print('%d paths found' % len(self.paths)) debug_print('END getting file paths on Kindle') return self.paths # Return the path to the reboot trigger file (SS/Fonts hack) def get_kindle_hack_reboot_trigger_file(self): fonts_hack_dir = os.path.join(self.root, 'linkfonts') ss_hack_dir = os.path.join(self.root, 'linkss') # If both hacks are installed, the Fonts hack watchdog will be the one used # (we're checking the pidfile, so this shouldn't really matter anymore, but let's continue doing it that way) reboot_file = '' if os.path.isfile(os.path.join(fonts_hack_dir, 'run', 'usb-watchdog.pid')): reboot_file = os.path.join(fonts_hack_dir, 'reboot') elif os.path.isfile(os.path.join(ss_hack_dir, 'run', 'usb-watchdog.pid')): reboot_file = os.path.join(ss_hack_dir, 'reboot') return reboot_file # Create the reboot trigger file for the SS/Fonts Hack - to force reboot when USB unplugged def create_kindle_hack_reboot_trigger_file(self): reboot_file = self.get_kindle_hack_reboot_trigger_file() if reboot_file: debug_print('Creating Kindle hack reboot trigger file: %s\n' % reboot_file) try: open(reboot_file, 'w').close except: raise ValueError('Unable to create the Kindle hack reboot trigger file "%s"' % reboot_file) def get_path_cache_filename(self): return os.path.join(self.root, PATH_CACHE_FILE) def read_path_info_cache(self): return self.read_json_file(self.get_path_cache_filename()) def get_cache_time(self): cache_time = 0 try: cache_time = os.stat(PATH_CACHE_FILE).st_mtime except: pass return cache_time def save_path_info_cache(self, data): json_file = self.get_path_cache_filename() debug_print('Trying to save path cache file: %s' % json_file) try: cf = open(json_file, 'w') except: debug_print('Unable to open path cache file for writing') else: try: json.dump(data, cf) cf.close debug_print('Path cache file saved') except: # Try to figure out why it failed, because that's bad (and a severe performance hit) import sys, traceback debug_print('Unable to save path cache file: {0} ({1})'.format(json_file, ''.join(traceback.format_exception(*sys.exc_info())[-2:]).strip().replace('\n',': '))) def get_reader_pref_filename(self): return os.path.join(self.root, READER_PREF_FILE) def read_reader_prefs(self): pref_file = self.get_reader_pref_filename() prefs = {} prefs['comments'] = '' try: for line in open(pref_file, 'r'): if line[0] == '#': prefs['comments'] += line else: (variable, value) = line.split('=', 1) if variable: prefs[variable] = six.text_type(value).strip() debug_print('Preference %s = %s' % (variable, prefs[variable])) else: debug_print('Unexpected line format: %s' % line) except: debug_print('Unable to read pref file') return prefs def write_reader_prefs(self, prefs): pref_file = self.get_reader_pref_filename() write_okay = True try: pf = open(pref_file, 'w') except: debug_print('Unable to open reader pref file for writing') write_okay = False else: try: if 'comments' in prefs: pf.write(prefs['comments']) for pref in sorted(prefs.keys()): if pref != 'comments': pf.write(pref + '=' + six.text_type(prefs[pref]) + '\n') pf.close() # except: except: write_okay = False debug_print('Unable to write data to reader pref file') return write_okay def get_font_dirname(self): return os.path.join(self.root, FONT_DIR) def get_font_filenames(self): names = [] font_dir = self.get_font_dirname() if os.path.isdir(font_dir): try: names = os.listdir(font_dir) except: debug_print('Unable to list font directory') else: debug_print('No custom fonts directory') file_names = [] for name in names: if os.path.isfile(os.path.join(font_dir, name)): file_names.append(name) return file_names def update_font_files(self, fontname): debug_print('Updating alt font files from "%s"' % fontname) font_dir = self.get_font_dirname() update_okay = True try: os.chdir(font_dir) except: update_okay = False debug_print('Unable to change to font directory %s' % font_dir) else: for t in [ 'Regular', 'Bold', 'Italic', 'BoldItalic' ]: try: copyfile(fontname + '-' + t + '.ttf', 'alt-' + t + '.ttf') except: update_okay = False debug_print('Unable to copy font file "%s"' % t) return update_okay PK P%B B kindle_modify_settings.py# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai from __future__ import absolute_import __license__ = 'GPL v3' __copyright__ = '2011-2018, meme' __docformat__ = 'restructuredtext en' ############################################################ # Kindle Edit window ############################################################ import re from functools import partial try: from PyQt5 import Qt as QtGui from PyQt5.Qt import Qt, QWidget, QVBoxLayout, QHBoxLayout, QLabel, \ QLineEdit, QTableWidget, QTableWidgetItem, \ QAbstractItemView, QToolButton, QDialog, QDialogButtonBox, \ QCheckBox, QGridLayout, QSpinBox, QGroupBox except: from PyQt4 import QtGui from PyQt4.Qt import Qt, QWidget, QVBoxLayout, QHBoxLayout, QLabel, \ QLineEdit, QTableWidget, QTableWidgetItem, \ QAbstractItemView, QToolButton, QDialog, QDialogButtonBox, \ QCheckBox, QGridLayout, QSpinBox, QGroupBox from calibre.gui2 import error_dialog, question_dialog import calibre_plugins.kindle_collections.config as cfg import calibre_plugins.kindle_collections.messages as msg import calibre_plugins.kindle_collections.kindle_device as kindle_device import calibre_plugins.kindle_collections.calibre_info as calibre_info import calibre_plugins.kindle_collections.save as save from calibre_plugins.kindle_collections.__init__ import PLUGIN_NAME from calibre_plugins.kindle_collections.utilities import debug_print, SizePersistedDialog, get_icon, \ ComboTableWidgetItem, CheckableTableWidgetItem, CheckableBoxWidgetItem, ReadOnlyTableWidgetItem READER_PREFS_DIALOG_GEOMETRY = 'kindle_collections plugin:edit settings dialog: geometry' DEFAULTS = { 'HORIZONTAL_MARGIN': '23', 'FONT_SIZE': '21', 'LINE_SPACING': '2', 'JUSTIFICATION': 'full', 'FONT_FAMILY': 'serif', 'DICTIONARY': '', 'LAST_BOOK_READ': '', 'ALLOW_JUSTIFICATION_CHANGE': 'false', 'ALLOW_TWO_COLUMN_VIEW': 'false', 'ALLOW_ARTICLE_THUMBNAIL': 'false', 'ALLOW_READING_INDICATOR': 'true', 'ALLOW_USER_FONT': 'false', 'ALLOW_USER_LINE_SPACING': 'false' } FONT_FAMILY_DEFAULTS = [ 'serif', 'condensed', 'sanserif' ] JUSTIFICATION_OPTIONS = [ 'full', 'left' ] ############################################################ def run(parent): debug_print('BEGIN Modifying Settings') gui = parent.gui if not cfg.init(parent): msg.message.display() debug_print('END Modifying Settings - Initialization failed') return # Make sure the settings are up to date, we make extensive use of model checking here... if not cfg.store.is_current_store_format(): msg.message.info('Please run "Customize collections" and select OK to save your customizations before running Modify Kindle Settings.') msg.message.display() return # Not supported on Kindle Touch/PaperWhite if cfg.config_settings['kindle_model_version'] < 5000: kindle_device.init(calibre_info.ci.device_path) reader_prefs = kindle_device.kdevice.read_reader_prefs() old_fontname = reader_prefs['FONT_FAMILY'] if 'FONT_FAMILY' in reader_prefs else None else: reader_prefs = {} old_fontname = None # Set defaults for all values for key in DEFAULTS.keys(): if key not in reader_prefs: reader_prefs[key] = DEFAULTS[key] # Get user fonts warnings = "" (font_family_options, warnings) = get_font_names() continue_modify = True if warnings != "": if not question_dialog(gui, _('Modify Settings - ' + PLUGIN_NAME), '
'+ warnings + '
Do you want to continue?', show_copy_button=False):
continue_modify = False
if continue_modify:
d = KindleEditDialog(gui, reader_prefs, font_family_options)
d.exec_()
if d.result() == d.Accepted:
# Get the preferences set in the dialog
debug_print('Saving preferences')
# Not supported on Kindle Touch/PaperWhite
if cfg.config_settings['kindle_model_version'] < 5000:
new_prefs = d.get_prefs()
update_font_files(old_fontname, new_prefs['FONT_FAMILY'])
if kindle_device.kdevice.write_reader_prefs(new_prefs):
save.save_fast_reboot()
else:
msg.message.error('Unable to write new preferences to the Kindle.')
else:
msg.message.error('Unavailable on the Kindle Touch/PaperWhite.')
msg.message.display()
debug_print('END Modifying Settings')
def update_font_files(old_fontname, new_fontname):
debug_print('Checking if font files need updating from "%s" to "%s"' % (old_fontname, new_fontname))
# Not supported on Kindle Touch/PaperWhite
if cfg.config_settings['kindle_model_version'] < 5000:
if new_fontname not in FONT_FAMILY_DEFAULTS and new_fontname != 'alt' and new_fontname != old_fontname:
if not kindle_device.kdevice.update_font_files(new_fontname):
msg.message.error('Unable to update font files.')
def get_font_names():
warnings = ""
custom_font_names = []
# Not supported on Kindle Touch/PaperWhite
if cfg.config_settings['kindle_model_version'] < 5000:
fontfiles = set(kindle_device.kdevice.get_font_filenames())
else:
fontfiles = None
if fontfiles:
fontnames = set([])
for filename in fontfiles:
fontname = re.sub('^(.*)-.*\.ttf$', r'\1', filename)
if fontname:
fontnames.add(fontname)
missing_any_variants = False
for fontname in fontnames:
if fontname != 'alt':
all_variants= True
for variant in [ 'Regular', 'Bold', 'Italic', 'BoldItalic' ]:
if fontname + '-' + variant + '.ttf' not in fontfiles:
all_variants = False
missing_any_variants = True
warnings += 'Font "%s" missing file "%s-%s.ttf"
' % (fontname, fontname, variant)
if all_variants:
custom_font_names.append(fontname)
debug_print('Found valid font %s' % fontname)
if missing_any_variants:
warnings += '
At least one custom font is missing a file in the Kindle fonts directory and will not be selectable.'
return (FONT_FAMILY_DEFAULTS + custom_font_names, warnings)
class KindleEditDialog(SizePersistedDialog):
def __init__(self, parent, prefs, font_options):
SizePersistedDialog.__init__(self, parent, READER_PREFS_DIALOG_GEOMETRY)
self.prefs = prefs
self.font_options = font_options
self.setWindowTitle(_('Modify Settings - ' + PLUGIN_NAME))
# Not supported on Kindle Touch/PaperWhite
if cfg.config_settings['kindle_model_version'] >= 5000:
button_box = QDialogButtonBox(QDialogButtonBox.Cancel)
else:
button_box = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
v = QVBoxLayout(self)
# Not supported on Kindle Touch/PaperWhite
if cfg.config_settings['kindle_model_version'] < 5000:
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
# Add the window for editing
v.addWidget(self.edit_widget())
v.addWidget(button_box)
self.resize_dialog()
def edit_widget(self):
self.edit_w = QWidget()
layout = QVBoxLayout(self.edit_w)
self.edit_w.setLayout(layout)
# Not supported on Kindle Touch/PaperWhite
if cfg.config_settings['kindle_model_version'] >= 5000:
warning_label = QLabel('Modify your Kindle settings.
Sorry, this is unavailable on your Kindle model.', self) warning_label.setToolTip('Unavailable on your Kindle model!') layout.addWidget(warning_label) else: warning_label = QLabel('Modify your Kindle settings.
Make sure you are on the Kindle\'s HOME screen before you connect the Kindle to your PC.', self)
warning_label.setToolTip('Click on the Kindle\'s Home button before connecting your Kindle to your PC.
Otherwise the settings may be ignored.')
# Settings
# Horizontal Margin
horizontal_margin_label = QLabel('Horizontal Margin:', self)
horizontal_margin_label.setToolTip('Kindle defaults:
fewest = 40
fewer = 80
default = 40')
self.horizontal_margin_spinbox = QSpinBox(self)
self.horizontal_margin_spinbox.setRange(0, 200)
self.horizontal_margin_spinbox.setValue(int(self.prefs['HORIZONTAL_MARGIN']))
horizontal_margin_label.setBuddy(self.horizontal_margin_spinbox)
# Justification
justification_label = QLabel('Justification:', self)
justification_label.setToolTip('Kindle default:
full
Set whether right margin is "jagged" (left) or "straight" (full). Does not work for books that override justification.') self.justification_combobox = ComboTableWidgetItem(self, self.prefs['JUSTIFICATION'], JUSTIFICATION_OPTIONS) justification_label.setBuddy(self.justification_combobox) # Line Spacing line_spacing_label= QLabel('Line Spacing:', self) line_spacing_label.setToolTip('Kindle defaults:
1, 2, 3
4 also appears to work. ') self.line_spacing_spinbox = QSpinBox(self) self.line_spacing_spinbox.setRange(1, 10) self.line_spacing_spinbox.setValue(int(self.prefs['LINE_SPACING'])) line_spacing_label.setBuddy(self.line_spacing_spinbox) # Font Size font_size_label = QLabel('Font Size:', self) font_size_label.setToolTip('Kindle defaults (from smallest "Aa" to largest):
17, 18, 21, 25, 31, 36, 60, 88') self.font_size_spinbox = QSpinBox(self) self.font_size_spinbox.setRange(1, 200) self.font_size_spinbox.setValue(int(self.prefs['FONT_SIZE'])) font_size_label.setBuddy(self.font_size_spinbox) # Font Family font_family_label = QLabel('Font Family:', self) font_family_label.setToolTip('Kindle choices:
serif (regular), condensed, sans serif
Choose the font to use on your Kindle.
See Help for this plugin to add additional fonts.')
self.font_family_combobox = ComboTableWidgetItem(self, self.prefs['FONT_FAMILY'], self.font_options)
font_family_label.setBuddy(self.font_family_combobox)
# Allows
self.allow_justification_change_checkbox = CheckableBoxWidgetItem(self.prefs['ALLOW_JUSTIFICATION_CHANGE'].lower() == 'true', 'Allow changing justification','Enable or disable showing the Justification choices (left/full) on the Kindle')
self.allow_user_line_spacing_checkbox = CheckableBoxWidgetItem(self.prefs['ALLOW_USER_LINE_SPACING'].lower() == 'true', 'Allow additional user line spacing options','If checked the line spacing options available on the Kindle change to read
"112 125-small 136-med 150-lg 162" giving up to 5 line spacing options instead of 3.')
self.allow_user_font_checkbox = CheckableBoxWidgetItem(self.prefs['ALLOW_USER_FONT'].lower() == 'true', 'Allow using user font','Must be checked to use a custom font or the font will be ignored even if selected.')
self.allow_reading_indicator_checkbox = CheckableBoxWidgetItem(self.prefs['ALLOW_READING_INDICATOR'].lower() == 'true', 'Allow reading indicator','Does not appear to actually change anything on the Kindle. Unfortunately this does not affect the reading indicator for books.')
self.allow_article_thumbnail_checkbox = CheckableBoxWidgetItem(self.prefs['ALLOW_ARTICLE_THUMBNAIL'].lower() == 'true', 'Allow article thumbnail','Does not appear to actually change anything on the Kindle.')
self.allow_two_column_view_checkbox = CheckableBoxWidgetItem(self.prefs['ALLOW_TWO_COLUMN_VIEW'].lower() == 'true', 'Allow two column view', 'Does not appear to actually change anything on the Kindle.')
# Settings Grid
settings_groupbox = QGroupBox('Kindle Settings')
settings_layout = QGridLayout()
settings_groupbox.setLayout(settings_layout)
settings_layout.addWidget(horizontal_margin_label, 0, 0, 1, 1)
settings_layout.addWidget(self.horizontal_margin_spinbox, 0, 1, 1, 1)
settings_layout.addWidget(justification_label, 1, 0, 1, 1)
settings_layout.addWidget(self.justification_combobox, 1, 1, 1, 1)
settings_layout.addWidget(line_spacing_label, 2, 0, 1, 1)
settings_layout.addWidget(self.line_spacing_spinbox, 2, 1, 1, 1)
settings_layout.addWidget(font_family_label, 3, 0, 1, 1)
settings_layout.addWidget(self.font_family_combobox, 3, 1, 1, 1)
settings_layout.addWidget(font_size_label, 4, 0, 1, 1)
settings_layout.addWidget(self.font_size_spinbox, 4, 1, 1, 1)
settings_layout.setColumnMinimumWidth(2,10)
settings_layout.addWidget(self.allow_justification_change_checkbox, 1, 3, 1, 1)
settings_layout.addWidget(self.allow_user_line_spacing_checkbox, 2, 3, 1, 1)
settings_layout.addWidget(self.allow_user_font_checkbox, 3, 3, 1, 1)
settings_layout.setColumnStretch(4,1)
unknown_settings_groupbox = QGroupBox('Kindle Settings with no known effect')
unknown_settings_layout = QGridLayout()
unknown_settings_groupbox.setLayout(unknown_settings_layout)
unknown_settings_layout.addWidget(self.allow_reading_indicator_checkbox, 0, 0, 1, 1)
unknown_settings_layout.addWidget(self.allow_article_thumbnail_checkbox, 1, 0, 1, 1)
unknown_settings_layout.addWidget(self.allow_two_column_view_checkbox, 2, 0, 1, 1)
settings_layout.setColumnStretch(1,1)
# Information
last_book_label = QLabel('Last Book Read: ', self.edit_w)
last_book_label.setToolTip('Read Only')
last_book2_label = QLabel(self.prefs['LAST_BOOK_READ'], self.edit_w)
dictionary_label = QLabel('Dictionary:', self.edit_w)
dictionary_label.setToolTip('Read Only')
dictionary2_label = QLabel(self.prefs['DICTIONARY'], self.edit_w)
# Information Grid
general_groupbox = QGroupBox('Kindle Information')
general_layout = QGridLayout()
general_groupbox.setLayout(general_layout)
general_layout.addWidget(last_book_label, 0, 0, 1, 1)
general_layout.addWidget(last_book2_label, 0, 1, 1, 2)
general_layout.addWidget(dictionary_label, 1, 0, 1, 1)
general_layout.addWidget(dictionary2_label, 1, 1, 1, 2)
general_layout.setColumnStretch(2,1)
# Show each section
layout.addWidget(warning_label)
layout.addSpacing(20)
layout.addWidget(settings_groupbox)
layout.addSpacing(10)
layout.addWidget(unknown_settings_groupbox)
layout.addSpacing(10)
layout.addWidget(general_groupbox)
layout.addStretch(1)
return self.edit_w
def get_prefs(self):
self.prefs['LINE_SPACING'] = self.line_spacing_spinbox.value()
self.prefs['JUSTIFICATION'] = JUSTIFICATION_OPTIONS[self.justification_combobox.currentIndex()]
self.prefs['FONT_SIZE'] = self.font_size_spinbox.value()
font_index = self.font_family_combobox.currentIndex()
if font_index >= 0:
self.prefs['FONT_FAMILY'] = self.font_options[font_index]
self.prefs['HORIZONTAL_MARGIN'] = self.horizontal_margin_spinbox.value()
self.prefs['ALLOW_JUSTIFICATION_CHANGE'] = 'true' if self.allow_justification_change_checkbox.checkState() else 'false'
self.prefs['ALLOW_USER_LINE_SPACING'] = 'true' if self.allow_user_line_spacing_checkbox.checkState() else 'false'
self.prefs['ALLOW_USER_FONT'] = 'true' if self.allow_user_font_checkbox.checkState() else 'false'
self.prefs['ALLOW_READING_INDICATOR'] = 'true' if self.allow_reading_indicator_checkbox.checkState() else 'false'
self.prefs['ALLOW_ARTICLE_THUMBNAIL'] = 'true' if self.allow_article_thumbnail_checkbox.checkState() else 'false'
self.prefs['ALLOW_TWO_COLUMN_VIEW'] = 'true' if self.allow_two_column_view_checkbox.checkState() else 'false'
return self.prefs
PK ;QgOqE qE ebook.py# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import absolute_import
from six.moves import range
from polyglot.builtins import is_py3
__license__ = 'GPL v3'
__copyright__ = '2011-2018, meme'
__docformat__ = 'restructuredtext en'
#####################################################################
# Kindle book parsing code
#####################################################################
import os, re, struct, hashlib, sys, traceback
from calibre import force_unicode
from calibre_plugins.kindle_collections.utilities import debug_print
import six
KINDLE_INTERNAL_ROOT = '/mnt/us'
#####################################################################
# c.f., https://github.com/kevinhendricks/KindleUnpack/blob/master/lib/compatibility_utils.py
if is_py3:
def bstr(s):
if isinstance(s, str):
return bytes(s, 'latin-1')
else:
return bytes(s)
def bord(s):
return s
else:
def bstr(s):
return str(s)
def bord(s):
return ord(s)
# Based on Mobipocket code
class EBook():
def __init__(self, path):
self.path = path
self.title = None
self.meta = None
self.author = None
self.asin = None
self.type = None
self.mobi_type = None
self.text_encoding = None
self.pubdate = None
self.collection_code = None
ext = os.path.splitext(self.path)[1][1:].lower()
self.error = None
if ext in ['mobi', 'pobi', 'azw', 'azw3', 'prc']:
try:
self.meta = Mobi(self.path)
except ValueError as e:
self.error = e
pass
else:
if self.meta.title:
self.title = self.meta.title
self.mobi_type = self.meta.mobi_type
self.text_encoding = self.meta.text_encoding
if 100 in self.meta.exth:
self.author = self.meta.exth[100]
if 106 in self.meta.exth:
self.pubdate = self.meta.exth[106]
if 113 in self.meta.exth:
self.asin = self.meta.exth[113]
if 501 in self.meta.exth:
self.type = self.meta.exth[501]
if 503 in self.meta.exth:
self.title = self.meta.exth[503]
elif ext in ['tpz', 'azw1']:
try:
self.meta = Topaz(self.path)
except ValueError as e:
self.error = e
pass
else:
if self.meta.title:
self.title = self.meta.title
if self.meta.asin:
self.asin = self.meta.asin
if self.meta.type:
self.type = self.meta.type
elif ext in ['azw2']:
try:
self.meta = Kindlet(self.path)
except ValueError as e:
pass
else:
if self.meta.title:
self.title = self.meta.title
if self.meta.asin:
self.asin = self.meta.asin
self.type = 'AZW2'
elif ext in ['kfx']:
try:
self.meta = Kfx(self.path)
except ValueError as e:
pass
else:
if self.meta.title:
self.title = self.meta.title
if self.meta.asin:
self.asin = self.meta.asin
if self.meta.authors:
self.author = self.meta.authors
if self.meta.pubdate:
self.pubdate = self.meta.pubdate
self.type = 'EBOK'
# i.e., UTF-8 in Mobi-speak ;).
self.text_encoding = 65001
# Change Kindle code to Amazon ASIN format if found
if self.error:
raise ValueError(self.error)
elif self.asin and self.asin != '' and self.title:
self.collection_code = "#{!s}^{!s}".format(force_unicode(self.asin, 'utf-8'), force_unicode(self.type, 'utf-8'))
elif os.path.isfile(self.path):
self.collection_code = "*{!s}".format(self.get_hash(self.get_internal_kindle_path(self.path)))
else:
raise ValueError('Unable to open file %s' % self.path)
# Returns SHA-1 hash
def get_hash(self, path):
return hashlib.sha1(path.encode('utf-8')).hexdigest()
# Returns the internal path (e.g. /mnt/us/somepath) for an absolute path to a Kindle file (converts '/' separator to current OS separator)
def get_internal_kindle_path(self, path):
path = os.path.normpath(path)
folder = os.path.dirname(path)
filename = os.path.basename(path)
return '/'.join([ KINDLE_INTERNAL_ROOT, re.sub(r'.*(documents|pictures|audible|music)', r'\1', folder), filename ]).replace('\\', '/')
# Based on MobiUnpack
class Sectionizer:
def __init__(self, filename):
try:
self.data = open(filename, 'rb').read()
except:
raise ValueError('Unable to open file %s' % filename)
else:
self.header = self.data[:78]
self.ident = self.header[0x3C:0x3C+8]
# Get title from old PalmDOC format files
if self.ident == b'BOOKMOBI':
try:
num_sections, = struct.unpack_from(b'>H', self.header, 76)
filelength = len(self.data)
sectionsdata = struct.unpack_from(bstr('>%dL' % (num_sections*2)), self.data, 78) + (filelength, 0)
self.sectionoffsets = sectionsdata[::2]
except:
debug_print("Unexpected error in reading Mobi book header information - unable to unpack: {}".format(sys.exc_info()[0]))
traceback.print_exc()
raise ValueError('Unexpected error in reading Mobi book header information - unable to unpack. Try using Calibre to reconvert the book to Mobi format (even if you need to convert from Mobi format) and resending to device')
elif self.ident != b'TEXtREAd':
raise ValueError('This book contains invalid Mobi book header information and cannot be read. Try using Calibre to reconvert the book to Mobi format (even if you need to convert from Mobi format) and resending it to the device')
return
def loadSection(self, section):
before, after = self.sectionoffsets[section:section+2]
return self.data[before:after]
# Mobi metadata parsing
class Mobi:
def __init__(self, filename):
self.title = None
self.mobi_type = None
self.text_encoding = None
try:
sections = Sectionizer(filename)
if sections.ident == b'TEXtREAd':
# Old Palm Doc format
self.title = sections.data[:32]
self.exth = []
else:
header = sections.loadSection(0)
length, self.mobi_type, self.text_encoding = struct.unpack(b'>LLL', header[20:32])
toff, tlen = struct.unpack(b'>II', header[0x54:0x5c])
tend = toff + tlen
self.title=header[toff:tend]
exth_flag, = struct.unpack(b'>L', header[0x80:0x84])
hasExth = exth_flag & 0x40
exth_rec = ''
exth_offset = length + 16
exth_length = 0
if hasExth:
exth_length, = struct.unpack_from(b'>L', header, exth_offset+4)
exth_length = ((exth_length + 3)>>2)<<2 # round to next 4 byte boundary
exth_rec = header[exth_offset:exth_offset+exth_length]
self.exth = dict()
if hasExth and exth_length != 0 and exth_rec != '':
num_items, = struct.unpack(b'>L', exth_rec[8:12])
pos = 12
self.exth[100] = []
for _ in range(num_items):
exth_id, size = struct.unpack(b'>LL', exth_rec[pos:pos+8])
# We only care about a few fields...
if exth_id in [100, 106, 113, 501, 503]:
contentsize = size - 8
content = exth_rec[pos+8:pos+size]
# For author, build a list, to support the way we now handle multiple authors (via multiple exth 100 fields)
if exth_id == 100:
self.exth[exth_id].append(content)
else:
self.exth[exth_id] = content
pos += size
# Join the list in a string to let force_unicode do its job properly later...
# NOTE: Bytes all the way down ;).
self.exth[100] = b';'.join(self.exth[100])
except ValueError as e:
raise ValueError(e)
except:
debug_print("Unexpected error in reading Mobi book header information: {}".format(sys.exc_info()[0]))
traceback.print_exc()
raise ValueError('Unexpected error in reading Mobi book header information. Try using Calibre to reconvert the book to Mobi format (even if you need to convert from Mobi format) and resending to device')
def zbyte(self, text):
for i in range(len(text)):
if text[i] == '\0':
break
return i
# Kindlet metadata parsing
class Kindlet:
def __init__(self, filename):
import zipfile, zipimport
# For official apps, ASIN is stored in the Amazon-ASIN field of META-INF/MANIFEST.MF, and title in the Implementation-Title field
try:
kindlet = zipfile.ZipFile( filename, 'r')
except:
raise ValueError('Unable to open file %s' % filename)
else:
kdkmanifest = kindlet.read( 'META-INF/MANIFEST.MF' )
# Catch Title
kdktitlem = re.search( b'(^Implementation-Title: )(.*?$)', kdkmanifest, re.MULTILINE )
if kdktitlem and kdktitlem.group(2):
self.title = kdktitlem.group(2).strip()
else:
self.title = None
# Catch ASIN
kdkasinm = re.search( b'(^Amazon-ASIN: )(.*?$)', kdkmanifest, re.MULTILINE )
if kdkasinm and kdkasinm.group(2):
self.asin = kdkasinm.group(2).strip()
else:
self.asin = None
kindlet.close()
# Topaz metadata parsing. Almost verbatim code by Greg Riker from Calibre
class StreamSlicer(object):
def __init__(self, stream, start=0, stop=None):
self._stream = stream
self.start = start
if stop is None:
stream.seek(0, 2)
stop = stream.tell()
self.stop = stop
self._len = stop - start
def __len__(self):
return self._len
def __getitem__(self, key):
stream = self._stream
base = self.start
if isinstance(key, six.integer_types):
stream.seek(base + key)
return stream.read(1)
if isinstance(key, slice):
start, stop, stride = key.indices(self._len)
if stride < 0:
start, stop = stop, start
size = stop - start
if size <= 0:
return b""
stream.seek(base + start)
data = stream.read(size)
if stride != 1:
data = data[::stride]
return data
raise TypeError('stream indices must be integers')
class Topaz(object):
def __init__(self, filename):
try:
self.stream = open(filename, 'rb')
except:
raise ValueError('Unable to open file %s' % filename)
else:
self.data = StreamSlicer(self.stream)
sig = self.data[:4]
if not sig.startswith(b'TPZ'):
raise ValueError('Not a valid Topaz file')
offset = 4
self.header_records, consumed = self.decode_vwi(self.data[offset:offset+4])
offset += consumed
self.topaz_headers = self.get_headers(offset)
# First integrity test - metadata header
if b'metadata' not in self.topaz_headers:
raise ValueError('Not a valid Topaz file, no metadata record')
# Second integrity test - metadata body
md_offset = self.topaz_headers[b'metadata']['blocks'][0]['offset']
md_offset += self.base
if self.data[md_offset+1:md_offset+9] != b'metadata':
raise ValueError('Not a valid Topaz file, damaged metadata record')
# Get metadata, and store what we need
try:
self.title, self.author, self.asin, self.type = self.get_metadata()
except:
debug_print("Unable to read metadata: {}".format(sys.exc_info()[0]))
traceback.print_exc()
raise ValueError('Unable to read metadata from file %s' % filename)
self.stream.close()
def decode_vwi(self, byts):
pos, val = 0, 0
done = False
byts = bytearray(byts)
while pos < len(byts) and not done:
b = byts[pos]
pos += 1
if (b & 0x80) == 0:
done = True
b &= 0x7F
val <<= 7
val |= b
if done:
break
return val, pos
def get_headers(self, offset):
# Build a dict of topaz_header records, list of order
topaz_headers = {}
for x in range(self.header_records):
offset += 1
taglen, consumed = self.decode_vwi(self.data[offset:offset+4])
offset += consumed
tag = self.data[offset:offset+taglen]
offset += taglen
num_vals, consumed = self.decode_vwi(self.data[offset:offset+4])
offset += consumed
blocks = {}
for val in range(num_vals):
hdr_offset, consumed = self.decode_vwi(self.data[offset:offset+4])
offset += consumed
len_uncomp, consumed = self.decode_vwi(self.data[offset:offset+4])
offset += consumed
len_comp, consumed = self.decode_vwi(self.data[offset:offset+4])
offset += consumed
blocks[val] = dict(offset=hdr_offset,len_uncomp=len_uncomp,len_comp=len_comp)
topaz_headers[tag] = dict(blocks=blocks)
self.eoth = self.data[offset]
offset += 1
self.base = offset
return topaz_headers
def get_metadata(self):
''' Return MetaInformation with title, author'''
self.get_original_metadata()
return force_unicode(self.metadata[b'Title'], 'utf-8'), force_unicode(self.metadata[b'Authors'], 'utf-8'), force_unicode(self.metadata[b'ASIN'], 'utf-8'), force_unicode(self.metadata[b'CDEType'], 'utf-8')
def get_original_metadata(self):
offset = self.base + self.topaz_headers[b'metadata']['blocks'][0]['offset']
self.md_header = {}
taglen, consumed = self.decode_vwi(self.data[offset:offset+4])
offset += consumed
self.md_header['tag'] = self.data[offset:offset+taglen]
offset += taglen
self.md_header['flags'] = ord(self.data[offset:offset+1])
offset += 1
self.md_header['num_recs'] = ord(self.data[offset:offset+1])
offset += 1
self.metadata = {}
for x in range(self.md_header['num_recs']):
taglen, consumed = self.decode_vwi(self.data[offset:offset+4])
offset += consumed
tag = self.data[offset:offset+taglen]
offset += taglen
md_len, consumed = self.decode_vwi(self.data[offset:offset+4])
offset += consumed
metadata = self.data[offset:offset + md_len]
offset += md_len
self.metadata[tag] = metadata
# KFX metadata parsing, c.f., Calibre's metadata_from_path @ devices/kindle/driver.py
# Originally implemented in KCP by stefano.sb (https://www.mobileread.com/forums/showpost.php?p=3731470&postcount=256)
class Kfx:
def __init__(self, filename):
from calibre.ebooks.metadata.kfx import read_metadata_kfx
mi = None
self.asin = None
self.title = None
self.authors = None
self.pubdate = None
try:
# Try the book itself first, for standalone, generated KFX files
kfx_path = filename
with lopen(kfx_path, 'rb') as f:
if f.read(8) != b'\xeaDRMION\xee':
f.seek(0)
mi = read_metadata_kfx(f)
else:
# Otherwise, look for the sidecar metadata file, for shipped KFX files
kfx_path = os.path.join(filename.rpartition('.')[0] + '.sdr', 'assets', 'metadata.kfx')
with lopen(kfx_path, 'rb') as mf:
mi = read_metadata_kfx(mf)
self.asin = mi.get_identifiers().get('mobi-asin')
self.title = mi.title
self.authors = mi.authors
# Date is a proper datetime object, while we only handle parsing string ourselves... Dumb it down.
self.pubdate = str(mi.pubdate)
except:
debug_print("Unable to parse KFX metadata: {}".format(sys.exc_info()[0]))
traceback.print_exc()
raise ValueError('Unable to parse KFX metadata from file %s' % filename)
PK
PI"= = kindle_view_collections.py# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import absolute_import
__license__ = 'GPL v3'
__copyright__ = '2011-2018, meme'
__docformat__ = 'restructuredtext en'
#####################################################################
# View collection report
#####################################################################
import re
import calibre_plugins.kindle_collections.config as cfg
import calibre_plugins.kindle_collections.messages as msg
import calibre_plugins.kindle_collections.kindle_device as kindle_device
import calibre_plugins.kindle_collections.calibre_info as calibre_info
import calibre_plugins.kindle_collections.kindle_collections as kindle_collections
import calibre_plugins.kindle_collections.kindle_books as kindle_books
import calibre_plugins.kindle_collections.save as save
import calibre_plugins.kindle_collections.reports as reports
from calibre_plugins.kindle_collections.kindle_sort import KindleSort
from calibre_plugins.kindle_collections.__init__ import PLUGIN_NAME, PLUGIN_VERSION
from calibre_plugins.kindle_collections.utilities import debug_print, wording
PERIODICAL_BACK_ISSUES = u'Periodicals: Back Issues'
#####################################################################
def run(parent):
debug_print('BEGIN View Report')
if not cfg.init(parent):
debug_print('END View Report - Initialization failed')
return msg.message
# Make sure the settings are up to date, we rely on the model being accurate here...
if not cfg.store.is_current_store_format():
msg.message.info('Please run "Customize collections" and select OK to save your customizations before running View Collections.')
return msg.message
calibre_info.ci.load_column_book_info()
kindle_device.init(calibre_info.ci.device_path)
kindle_collections.init(kindle_device.kdevice.get_collections())
kindle_paths = kindle_device.kdevice.get_file_paths()
# Check that books were found on the Kindle
if len(kindle_paths) < 1:
msg.message.error('No books were found on the Kindle.
If you know your books are on the device, try disconnecting/reconnecting your Kindle or restarting Calibre.')
return msg.message
kindle_books.init(kindle_paths)
generate_existing_kindle_collections_report()
debug_print('END View Report')
return msg.message
def generate_existing_kindle_collections_report(show_collection_count=True):
msg.message.header('\n=== %s %s View Collections Report\n' % (PLUGIN_NAME, PLUGIN_VERSION))
if show_collection_count:
msg.message.info('%s' % save.get_summary())
if kindle_collections.kc.get_len() <= 0:
msg.message.report('There are no collections on the Kindle.\n\nUse Create or Edit to save collections to the Kindle.')
else:
ksort = KindleSort()
# Report Sort by Collections
msg.message.report('\nCollections sorted by Most Recent (Kindle view "Sort by Collections"):\n')
# Show the collections
for collection in kindle_collections.kc.get_time_sorted_names():
msg.message.report(' %s (%d)' % (collection, kindle_books.kbooks.get_visible_book_count(collection)))
# Show Periodical Back Issues last since we can't tell its update time
if kindle_books.kbooks.periodical_back_issues:
msg.message.report(' %s (%d)' % (PERIODICAL_BACK_ISSUES, len(kindle_books.kbooks.periodical_back_issues)))
# Report Sort by Title
msg.message.report('\n\nCollections and Books sorted by Title (Kindle view "Sort by Title"):\n')
# Add author or date to end of book names
book_info = []
for (title, info) in kindle_books.kbooks.get_visible_unsorted_titles_and_info():
title_string = '%-80s %s' % (title, info)
book_info.append(title_string)
col_names = ksort.sort_names(kindle_collections.kc.get_unsorted_names() + book_info + [ PERIODICAL_BACK_ISSUES ])
for name in col_names:
if not re.match(' Book with no File - ', name):
if name == PERIODICAL_BACK_ISSUES:
title = '%s (%d)' % (PERIODICAL_BACK_ISSUES, len(kindle_books.kbooks.periodical_back_issues))
msg.message.report(' %-80s If you know your books are on the device, try disconnecting/reconnecting your Kindle or restarting Calibre.'), '', show=True)
return
kindle_books.init(kindle_paths)
useable_columns = get_useable_custom_columns()
if len(useable_columns) > 0:
column_names = list(useable_columns.keys())
default_field = get_pref(PREFS_COLUMN_DEFAULT)
if not default_field:
default_field = column_names[0]
d = KindleImportDialog(gui, column_names, default_field)
d.exec_()
if d.result() == d.Accepted:
idx = d.w.collection_combo.currentIndex()
if idx >= 0:
column_name = column_names[idx]
column_label = useable_columns[column_name]['label']
column_type = useable_columns[column_name]['type']
column_is_multiple = useable_columns[column_name]['is_multiple']
ok = True
if calibre_info.ci.is_data_in_column(column_label):
if not question_dialog(gui, _('Import ' + PLUGIN_NAME), _('Custom column "%s" contains data. All data in the custom column will be replaced. Are you sure you want to continue?' % column_name), show_copy_button=False):
ok = False
if ok:
debug_print('Continuing import to column "%s"' % column_name)
debug_print('Building list from %d books in Calibre on device' % len(calibre_info.ci.ids_on_device))
set_pref(PREFS_COLUMN_DEFAULT, column_name)
id_collections = {}
for id in calibre_info.ci.ids_on_device:
lpath = calibre_info.ci.id_map[id].lpath
collections = []
if lpath in kindle_books.kbooks.path_info:
code = kindle_books.kbooks.path_info[lpath]['code']
collections = kindle_collections.kc.get_collections_for_code(code)
if column_type == 'bool':
id_collections[id] = True if len(collections) > 0 else None
else:
# If column contains ",", convert to ";" - hack to allow Author Sort names to be imported/exported
new_collections = [ collection.replace(',', ';') for collection in collections ]
id_collections[id] = ','.join(new_collections)
debug_print('%d book entries to update' % len(calibre_info.ci.ids_on_device))
debug_print('Importing to column %s' % column_label)
for id in id_collections:
debug_print(' id %s, collections=%s' % (id, id_collections[id]))
if len(id_collections) < 1:
msg.message.error('No collections for books found on the Kindle - import aborted. If you know there are books on your Kindle and in Calibre, try disconnecting/reconnecting your Kindle or restarting Calibre.')
else:
calibre_info.ci.clear_custom_column(column_label)
calibre_info.ci.set_custom_values(id_collections, column_label)
msg.message.info('Collections imported into custom column "%s".' % column_name)
msg.message.display()
else:
msg.message.info('No custom column selected, nothing imported.')
msg.message.display()
else:
error_dialog(gui, _('Import ' + PLUGIN_NAME), _('Unable to import Kindle collections into Calibre. Create at least one custom column, using Preferences->Add your own column, from these types: For example: You can use custom columns of type text, comma separated text, long text, or yes/no.')
heading_layout = QHBoxLayout()
heading_layout.addWidget(heading_label)
layout.addLayout(heading_layout)
# Add Collection row at top
button_layout = QHBoxLayout()
layout.addLayout(button_layout)
c_label = QLabel('Custom Column:', self.w)
c_label.setToolTip('Click the collection drop down box to select a collection on the Kindle')
button_layout.addWidget(c_label)
# Add the Collection combobox - will be populated later
self.w.collection_combo = ComboTableWidgetItem(self.w,default_field, fields)
self.w.collection_combo.setToolTip('Click this drop down box to select a collection on the Kindle')
self.w.collection_combo.setMinimumSize(200, 20)
button_layout.addWidget(self.w.collection_combo)
button_layout.addStretch(1)
layout.addStretch(1)
# Add the top piece
v.addWidget(self.w)
# Add the selection buttons
v.addWidget(button_box)
self.resize_dialog()
PK PkH kindle_restore_collections.py# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import absolute_import
__license__ = 'GPL v3'
__copyright__ = '2011-2018, meme'
__docformat__ = 'restructuredtext en'
#####################################################################
# Restore collection file on Kindle
#####################################################################
from calibre.gui2 import question_dialog
import calibre_plugins.kindle_collections.messages as msg
import calibre_plugins.kindle_collections.config as cfg
import calibre_plugins.kindle_collections.calibre_info as calibre_info
import calibre_plugins.kindle_collections.kindle_device as kindle_device
from calibre_plugins.kindle_collections.utilities import debug_print
from calibre_plugins.kindle_collections.__init__ import PLUGIN_NAME
#####################################################################
# Menu item Restore
def run(parent):
debug_print('BEGIN Restore collections file')
gui = parent.gui
if not cfg.init(parent):
msg.message.display()
debug_print('END Restore collections File - initialization failed')
return False
# Make sure the settings are up to date, we rely on the model being accurate here...
if not cfg.store.is_current_store_format():
msg.message.info('Please run "Customize collections" and select OK to save your customizations before running Restore Collections.')
msg.message.display()
return False
calibre_info.ci.load_column_book_info()
kindle_device.init(calibre_info.ci.device_path)
# Depending on the settings, this might be a bad idea on >= K5, warn about it.
if cfg.config_settings['kindle_model_version'] >= 5000:
restore_dialog_body = 'Are you sure you want to restore the previous Collections file saved on the Kindle?' + ' ' + 'Note that, with your Kindle model, this might be counterproductive, depending on your settings, and how you use CM/LS!'
else:
restore_dialog_body = 'Are you sure you want to restore the previous Collections file saved on the Kindle?'
if question_dialog(gui, _('Restore ' + PLUGIN_NAME), ' '+
restore_dialog_body, show_copy_button=False):
try:
kindle_device.kdevice.restore_collections()
except ValueError as e:
msg.message.error(str(e))
else:
msg.message.info('Collections restored.')
msg.message.display()
debug_print('END Restore collections File')
PK PA, , calibre_info.py# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import absolute_import
import six
__license__ = 'GPL v3'
__copyright__ = '2011-2018, meme'
__docformat__ = 'restructuredtext en'
##########################################################################
# Calibre general information
##########################################################################
from collections import defaultdict
from calibre.gui2.actions import InterfaceAction
#import calibre_plugins.kindle_collections.config as cfg
import calibre_plugins.kindle_collections.messages as msg
from calibre_plugins.kindle_collections.utilities import debug_print
from calibre.utils.localization import calibre_langcode_to_name
CALIBRE_COLUMNS_SHOW = [ 'authors', 'publisher', 'series', 'tags', 'author_sort', 'title', 'timestamp', 'languages', 'rating' ]
device_metadata_available = False
ci = None
##########################################################################
def init(parent):
global ci
ci = CalibreInfo(parent)
class CalibreInfo():
def __init__ (self, parent):
debug_print('BEGIN Initialize CalibreInfo')
self.gui = parent.gui
self.db = self.gui.library_view.model().db
self.device_model = self.gui.memory_view.model() # Kindle only has one memory location - no cards
self.library_uuid = self.get_library_uuid()
self.device_uuid = self.get_device_uuid()
self.device_path = self.get_device_path()
self.get_columns()
debug_print('END Initialize CalibreInfo')
def get_library_uuid(self):
debug_print('BEGIN Library uuid')
try:
library_uuid = self.gui.library_view.model().db.library_id
except:
library_uuid = ''
debug_print('END Library uuid: %s' % library_uuid)
return library_uuid
def get_device_uuid(self):
debug_print('BEGIN Device uuid')
try:
device_connected = self.gui.library_view.model().device_connected
device_uuid = self.gui.device_manager.connected_device.driveinfo['main']['device_store_uuid']
except:
device_uuid = ''
debug_print('END Device uuid: %s' % device_uuid)
return device_uuid
# Check if Kindle is connected - only debug messages since messages are initialized just after
def get_device_path(self):
from calibre.devices.usbms.driver import USBMS
debug_print('BEGIN Get Device Path')
device_path = ''
try:
# If we're in test mode TEST_DEVICE is defined, use the predefined test directory
#TEST_DEVICE = 'fakeKindleDir2'
device_path = TEST_DEVICE
debug_print('RUNNING IN TEST MODE')
except:
# Not in test mode, so confirm a device is connected
device_connected = self.gui.library_view.model().device_connected
try:
device_connected = self.gui.library_view.model().device_connected
except:
debug_print('No device connected')
device_connected = None
# If there is a device connected, test if we can retrieve the mount point from Calibre
if device_connected is not None:
try:
# _main_prefix is not reset when device is ejected so must be sure device_connected above
device_path = self.gui.device_manager.connected_device._main_prefix
debug_print('Root path of device: %s' % device_path)
except:
debug_print('A device appears to be connected, but device path not defined')
else:
debug_print('No device appears to be connected')
debug_print('END Get Device Path')
return device_path
def get_custom_columns(self):
return self.db.field_metadata.custom_field_metadata()
def set_custom_values(self, id_list, label):
debug_print('BEGIN Filling custom column %s' % label)
self.db.new_api.set_field(label, {id:id_list[id] for id in id_list.keys()})
# Refresh GUI
debug_print('Refreshing the GUI')
self.gui.iactions['Edit Metadata'].refresh_gui(list(id_list.keys()), covers_changed=False)
debug_print('END Filling custom column %s' % label)
def clear_custom_column(self, label):
debug_print('BEGIN Clearing custom column %s' % label)
self.db.new_api.set_field(label, {id:None for id in self.all_ids})
debug_print('END Clearing custom column %s' % label)
def is_data_in_column(self, label):
found_data = False
column_data = self.db.new_api.all_field_for(label, self.all_ids)
for id in column_data.keys():
if column_data[id]:
found_data = True
break
return found_data
# Build list of selected built-in column label/names, all custom column label/names and add entry for user categories
def get_columns(self):
debug_print('BEGIN Calibre get_columns')
self.custom_columns = self.db.custom_field_keys()
# Loop through each field entry and save field label and name
self.column_labels = {}
name_label = {}
# Custom field keys start with '#'
fields = self.db.custom_field_keys() + CALIBRE_COLUMNS_SHOW
for i in fields:
name = ''
try:
meta = self.db.metadata_for_field(i)
name = meta['name']
except:
pass
# Author Sort is the one column without a name in Calibre as its an internal column
if i == 'author_sort':
name = 'Author Sort'
debug_print('Including Calibre column label "%s", name "%s"' % (i, name))
if not name or name in name_label:
name = six.text_type(i)
debug_print('Duplicate or missing name, reset field %s to name "%s"' % (i, name))
self.column_labels[i] = name
name_label[name] = i
# Special entry for user categories
if 'User Categories' in name_label:
self.column_labels['user_categories'] = 'Calibre User Categories_'
else:
self.column_labels['user_categories'] = 'User Categories'
debug_print('END Calibre get_columns')
# Build a list of collections/column names and what books they contain, and save column details per book
def load_column_book_info(self):
debug_print('BEGIN Calibre Load Book Info')
self.active_collections = {}
self.lpath_info = {}
# Get a list of book ids on the device from the current library
debug_print('Getting ids')
query = 'ondevice:True'
self.ids_on_device = self.db.search_getting_ids(query, None)
query = ''
self.all_ids = self.db.search_getting_ids(query, None)
debug_print('%d ids, %d on device' % (len(self.all_ids), len(self.ids_on_device)))
# Get the list of book data using those ids in the library
# Throw away the row value in the (row, book) tuple returned
debug_print('Getting books')
calibre_books = []
calibre_books.append([tup[1] for tup in self.device_model.paths_for_db_ids(self.all_ids)])
# Create a map from the id to the book data
debug_print('Mapping id to book')
self.id_map = defaultdict(set)
for blist in calibre_books:
for abook in blist:
self.id_map[abook.application_id] = abook
debug_print('%d ids mapped' % len(self.id_map))
debug_print('Loading book details for each book from Calibre')
for id in self.all_ids:
mi = self.db.get_metadata(id, index_is_id=True)
# Save pathname only for books on the device so we know which paths are in calibre
lpath = ''
ond = id in self.ids_on_device
debug_print('CALIBRE: on device: "%s" - %s' % (ond, mi.title))
if id in self.ids_on_device:
lpath = self.id_map[id].lpath
if lpath:
# Save the details for books on the device
self.lpath_info[lpath] = { 'id': id, 'tags': mi.tags, 'authors': mi.authors, 'title': mi.title, 'author_sort': mi.author_sort }
else:
debug_print('Unexpected error: lpath missing from id %s' % id)
# Save column values for every book in Calibre because we need to know all Calibre 'collection' names to be able to delete unused names
# Built-in columns
for field in CALIBRE_COLUMNS_SHOW:
data = mi.get(field)
self.add_path(field, data, lpath)
# Custom columns
custom_data = mi.get_all_user_metadata(False)
for label in custom_data.keys():
if custom_data[label] and '#value#' in custom_data[label]:
self.add_path(label, custom_data[label]['#value#'], lpath)
# User categories
if mi.user_categories:
for category in mi.user_categories.keys():
if mi.user_categories[category]:
self.add_path('user_categories', category, lpath)
debug_print('END Calibre Load Book Info')
# Add the path of one book to a collection/column only if it is on the Kindle (path not none)
def add_path(self, column, collection, path):
collections = collection if type(collection) == list else [ collection ]
for c in collections:
if c:
# If we added a language or a rating, use a human readable string instead of the internal representation...
if column == 'languages':
c = calibre_langcode_to_name(c)
elif column == 'rating':
c = u'%.2g Stars' %(float(c)/2.0)
if column in self.active_collections:
if c in self.active_collections[column]:
if path:
self.active_collections[column][c].append(path)
else:
self.active_collections[column][c] = [ path ] if path else []
else:
self.active_collections[column] = { c: [ path ] } if path else { c: [] }
def get_personal_doc_tag(self):
from calibre.ebooks.conversion.config import load_defaults
prefs = load_defaults('mobi_output')
return prefs.get('personal_doc', None)
# No printing in this routine as it can be called before the plugin is run
def device_connection_changed_signalled(device_connected):
global device_metadata_available
if not device_connected:
device_metadata_available = False
# debug_print('Kindle Collections received signal connection changed: device not connected and setting device_metadata_available = False')
#else:
# debug_print('Kindle Collections received signal connection changed: device connected, available = %s' % device_metadata_available)
# No printing in this routine as its called before the plugin is run
def device_metadata_available_signalled():
global device_metadata_available
device_metadata_available = True
#debug_print('Kindle Collections received signal metadata available: setting device_metadata_available = True')
PK PF F about.py# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import absolute_import
__license__ = 'GPL v3'
__copyright__ = '2011-2018, meme'
__docformat__ = 'restructuredtext en'
#####################################################################
# Help
#####################################################################
from calibre_plugins.kindle_collections.utilities import debug_print
from calibre.gui2 import info_dialog
from calibre_plugins.kindle_collections.__init__ import PLUGIN_NAME, PLUGIN_VERSION
#####################################################################
# Display help information
def run(parent):
debug_print('BEGIN About')
info = ' ' + PLUGIN_NAME + ' ' + PLUGIN_VERSION
dialog = info_dialog(parent.gui, _('About plugin'), _(info), '', show=True)
debug_print('END About')
PK AQ images/ PK Gy`J J images/kindle_collections.pngPNG
IHDR >a gAMA a cHRM z&
-- Text, column shown in the tag browser
-- Comma separated text, like tags
-- Long text, like comments
-- Yes/No
-- Lookup name: "importedkindlecollections"
-- Column heading: "Imported Kindle Collections"
-- Column type: "Comma separated text, like tags"'), '', show=True)
debug_print('END Import Kindle collections')
def get_useable_custom_columns():
useable_columns = {}
all_custom_columns = calibre_info.ci.get_custom_columns()
for column in all_custom_columns:
col = all_custom_columns[column]
col_name = col['name']
typ = col['datatype']
is_multiple = col['is_multiple'] != None
debug_print('Checking column=%s, Name="%s", Type=%s, Multiple=%s' % (column, col_name, typ, is_multiple))
if typ in ('text', 'comments', 'bool'):
useable_columns[col_name] = { 'label': column, 'type': typ, 'is_multiple': is_multiple }
debug_print(' OK for import')
else:
debug_print(' Cannot be used for import')
return useable_columns
class KindleImportDialog(SizePersistedDialog):
def __init__(self, parent, fields, default_field):
SizePersistedDialog.__init__(self, parent, PREFS_DIALOG_GEOMETRY)
self.setWindowTitle(_('Import ' + PLUGIN_NAME))
v = QVBoxLayout(self)
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
self.w = QWidget()
layout = QVBoxLayout(self.w)
self.w.setLayout(layout)
# Add a comment describing the window
heading_label = QLabel('Import all Kindle collection names into the Calibre Custom Column listed below:', self.w)
heading_label.setToolTip('Select the custom column you created to import to. All existing entries will be overwritten.