summaryrefslogtreecommitdiffstats
path: root/Obok_calibre_plugin
diff options
context:
space:
mode:
authorApprentice Alf <[email protected]>2015-03-13 07:16:59 +0000
committerApprentice Alf <[email protected]>2015-03-13 07:16:59 +0000
commitcf922b6ba1e6acde04fadaa979d9e44a14449fb3 (patch)
tree24f9e7b94f685ad6bb0e34b93c266a27dd6c5b66 /Obok_calibre_plugin
parent9d9c879413e8ea2bc805399c15f2e94b02f48e1d (diff)
obok 3.1.1 plugin unzipped
Diffstat (limited to 'Obok_calibre_plugin')
-rw-r--r--Obok_calibre_plugin/obok_plugin/__init__.py75
-rw-r--r--Obok_calibre_plugin/obok_plugin/action.py474
-rw-r--r--Obok_calibre_plugin/obok_plugin/common_utils.py589
-rw-r--r--Obok_calibre_plugin/obok_plugin/config.py40
-rw-r--r--Obok_calibre_plugin/obok_plugin/default.po335
-rw-r--r--Obok_calibre_plugin/obok_plugin/dialogs.py455
-rw-r--r--Obok_calibre_plugin/obok_plugin/images/obok.pngbin0 -> 2858 bytes
-rw-r--r--Obok_calibre_plugin/obok_plugin/obok/__init__.py4
-rw-r--r--Obok_calibre_plugin/obok_plugin/obok/legacy_obok.py71
-rw-r--r--Obok_calibre_plugin/obok_plugin/obok/obok.py482
-rw-r--r--Obok_calibre_plugin/obok_plugin/obok_dedrm_Help.htm37
-rw-r--r--Obok_calibre_plugin/obok_plugin/plugin-import-name-obok_dedrm.txt0
-rw-r--r--Obok_calibre_plugin/obok_plugin/translations/de.po102
-rw-r--r--Obok_calibre_plugin/obok_plugin/translations/default.po335
-rw-r--r--Obok_calibre_plugin/obok_plugin/translations/es.po419
-rw-r--r--Obok_calibre_plugin/obok_plugin/translations/nl.po102
-rw-r--r--Obok_calibre_plugin/obok_plugin/utilities.py228
17 files changed, 3748 insertions, 0 deletions
diff --git a/Obok_calibre_plugin/obok_plugin/__init__.py b/Obok_calibre_plugin/obok_plugin/__init__.py
new file mode 100644
index 0000000..7b67e7a
--- /dev/null
+++ b/Obok_calibre_plugin/obok_plugin/__init__.py
@@ -0,0 +1,75 @@
+# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
+from __future__ import (unicode_literals, division, absolute_import,
+ print_function)
+
+__license__ = 'GPL v3'
+__docformat__ = 'restructuredtext en'
+
+#####################################################################
+# Plug-in base class
+#####################################################################
+
+from calibre.customize import InterfaceActionBase
+
+try:
+ load_translations()
+except NameError:
+ pass # load_translations() added in calibre 1.9
+
+PLUGIN_NAME = 'Obok DeDRM'
+PLUGIN_SAFE_NAME = PLUGIN_NAME.strip().lower().replace(' ', '_')
+PLUGIN_DESCRIPTION = _('Removes DRM from Kobo kepubs and adds them to the library.')
+PLUGIN_VERSION_TUPLE = (3, 1, 1)
+PLUGIN_VERSION = '.'.join([str(x) for x in PLUGIN_VERSION_TUPLE])
+HELPFILE_NAME = PLUGIN_SAFE_NAME + '_Help.htm'
+PLUGIN_AUTHORS = 'Anon'
+#####################################################################
+
+class ObokDeDRMAction(InterfaceActionBase):
+
+ name = PLUGIN_NAME
+ description = PLUGIN_DESCRIPTION
+ supported_platforms = ['windows', 'osx']
+ author = PLUGIN_AUTHORS
+ version = PLUGIN_VERSION_TUPLE
+ minimum_calibre_version = (1, 0, 0)
+
+ #: This field defines the GUI plugin class that contains all the code
+ #: that actually does something. Its format is module_path:class_name
+ #: The specified class must be defined in the specified module.
+ actual_plugin = 'calibre_plugins.'+PLUGIN_SAFE_NAME+'.action:InterfacePluginAction'
+
+ def is_customizable(self):
+ '''
+ This method must return True to enable customization via
+ Preferences->Plugins
+ '''
+ return True
+
+ def config_widget(self):
+ '''
+ Implement this method and :meth:`save_settings` in your plugin to
+ use a custom configuration dialog.
+
+ This method, if implemented, must return a QWidget. The widget can have
+ an optional method validate() that takes no arguments and is called
+ immediately after the user clicks OK. Changes are applied if and only
+ if the method returns True.
+
+ If for some reason you cannot perform the configuration at this time,
+ return a tuple of two strings (message, details), these will be
+ displayed as a warning dialog to the user and the process will be
+ aborted.
+
+ The base class implementation of this method raises NotImplementedError
+ so by default no user configuration is possible.
+ '''
+ if self.actual_plugin_:
+ from calibre_plugins.obok_dedrm.config import ConfigWidget
+ return ConfigWidget(self.actual_plugin_)
+
+ def save_settings(self, config_widget):
+ '''
+ Save the settings specified by the user with config_widget.
+ '''
+ config_widget.save_settings()
diff --git a/Obok_calibre_plugin/obok_plugin/action.py b/Obok_calibre_plugin/obok_plugin/action.py
new file mode 100644
index 0000000..2af4eb6
--- /dev/null
+++ b/Obok_calibre_plugin/obok_plugin/action.py
@@ -0,0 +1,474 @@
+# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
+from __future__ import (unicode_literals, division, absolute_import,
+ print_function)
+
+__license__ = 'GPL v3'
+__docformat__ = 'restructuredtext en'
+
+
+import os, zipfile
+
+try:
+ from PyQt5.Qt import QToolButton, QUrl
+except ImportError:
+ from PyQt4.Qt import QToolButton, QUrl
+
+from calibre.gui2 import open_url, question_dialog
+from calibre.gui2.actions import InterfaceAction
+from calibre.utils.config import config_dir
+from calibre.ptempfile import (PersistentTemporaryDirectory,
+ PersistentTemporaryFile, remove_dir)
+
+from calibre.ebooks.metadata.meta import get_metadata
+
+from calibre_plugins.obok_dedrm.dialogs import (SelectionDialog, DecryptAddProgressDialog,
+ AddEpubFormatsProgressDialog, ResultsSummaryDialog)
+from calibre_plugins.obok_dedrm.config import plugin_prefs as cfg
+from calibre_plugins.obok_dedrm.__init__ import (PLUGIN_NAME, PLUGIN_SAFE_NAME,
+ PLUGIN_VERSION, PLUGIN_DESCRIPTION, HELPFILE_NAME)
+from calibre_plugins.obok_dedrm.utilities import (
+ get_icon, set_plugin_icon_resources, showErrorDlg, format_plural,
+ debug_print
+ )
+
+from calibre_plugins.obok_dedrm.obok.obok import KoboLibrary
+from calibre_plugins.obok_dedrm.obok.legacy_obok import legacy_obok
+
+PLUGIN_ICONS = ['images/obok.png']
+
+try:
+ debug_print("obok::action_err.py - loading translations")
+ load_translations()
+except NameError:
+ debug_print("obok::action_err.py - exception when loading translations")
+ pass # load_translations() added in calibre 1.9
+
+class InterfacePluginAction(InterfaceAction):
+ name = PLUGIN_NAME
+ action_spec = (PLUGIN_NAME, None,
+ _(PLUGIN_DESCRIPTION), None)
+ popup_type = QToolButton.InstantPopup
+ action_type = 'current'
+
+ def genesis(self):
+ icon_resources = self.load_resources(PLUGIN_ICONS)
+ set_plugin_icon_resources(PLUGIN_NAME, icon_resources)
+
+ self.qaction.setIcon(get_icon(PLUGIN_ICONS[0]))
+ self.qaction.triggered.connect(self.launchObok)
+ self.gui.keyboard.finalize()
+
+ def launchObok(self):
+ '''
+ Main processing/distribution method
+ '''
+ self.count = 0
+ self.books_to_add = []
+ self.formats_to_add = []
+ self.add_books_cancelled = False
+ self.decryption_errors = []
+ self.userkeys = []
+ self.duplicate_book_list = []
+ self.no_home_for_book = []
+ self.ids_of_new_books = []
+ self.successful_format_adds =[]
+ self.add_formats_cancelled = False
+ self.tdir = PersistentTemporaryDirectory('_obok', prefix='')
+ self.db = self.gui.current_db.new_api
+ self.current_idx = self.gui.library_view.currentIndex()
+
+ print ('Running {}'.format(PLUGIN_NAME + ' v' + PLUGIN_VERSION))
+ # Get the Kobo Library object (obok v3.01)
+ self.library = KoboLibrary()
+
+ # Get a list of Kobo titles
+ books = self.build_book_list()
+ if len(books) < 1:
+ msg = _('<p>No books found in Kobo Library\nAre you sure it\'s installed\configured\synchronized?')
+ showErrorDlg(msg, None)
+ return
+
+ # Check to see if a key can be retrieved using the legacy obok method.
+ legacy_key = legacy_obok().get_legacy_cookie_id
+ if legacy_key is not None:
+ print (_('Legacy key found: '), legacy_key.encode('hex_codec'))
+ self.userkeys.append(legacy_key)
+ # Add userkeys found through the normal obok method to the list to try.
+ try:
+ candidate_keys = self.library.userkeys
+ except:
+ print (_('Trouble retrieving keys with newer obok method.'))
+ else:
+ if len(candidate_keys):
+ self.userkeys.extend(candidate_keys)
+ print (_('Found {0} possible keys to try.').format(len(self.userkeys)))
+ if not len(self.userkeys):
+ msg = _('<p>No userkeys found to decrypt books with. No point in proceeding.')
+ showErrorDlg(msg, None)
+ return
+
+ # Launch the Dialog so the user can select titles.
+ dlg = SelectionDialog(self.gui, self, books)
+ if dlg.exec_():
+ books_to_import = dlg.getBooks()
+ self.count = len(books_to_import)
+ debug_print("InterfacePluginAction::launchObok - number of books to decrypt: %d" % self.count)
+ # Feed the titles, the callback function (self.get_decrypted_kobo_books)
+ # and the Kobo library object to the ProgressDialog dispatcher.
+ d = DecryptAddProgressDialog(self.gui, books_to_import, self.get_decrypted_kobo_books, self.library, 'kobo',
+ status_msg_type='Kobo books', action_type=('Decrypting', 'Decryption'))
+ # Canceled the decryption process; clean up and exit.
+ if d.wasCanceled():
+ print (_('{} - Decryption canceled by user.').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION))
+ self.library.close()
+ remove_dir(self.tdir)
+ return
+ else:
+ # Canceled the selection process; clean up and exit.
+ self.library.close()
+ remove_dir(self.tdir)
+ return
+ # Close Kobo Library object
+ self.library.close()
+
+ # If we have decrypted books to work with, feed the list of decrypted books details
+ # and the callback function (self.add_new_books) to the ProgressDialog dispatcher.
+ if len(self.books_to_add):
+ d = DecryptAddProgressDialog(self.gui, self.books_to_add, self.add_new_books, self.db, 'calibre',
+ status_msg_type='new calibre books', action_type=('Adding','Addition'))
+ # Canceled the "add new books to calibre" process;
+ # show the results of what got added before cancellation.
+ if d.wasCanceled():
+ print (_('{} - "Add books" canceled by user.').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION))
+ self.add_books_cancelled = True
+ print (_('{} - wrapping up results.').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION))
+ self.wrap_up_results()
+ remove_dir(self.tdir)
+ return
+ # If books couldn't be added because of duplicate entries in calibre, ask
+ # if we should try to add the decrypted epubs to existing calibre library entries.
+ if len(self.duplicate_book_list):
+ if cfg['finding_homes_for_formats'] == 'Always':
+ self.process_epub_formats()
+ elif cfg['finding_homes_for_formats'] == 'Never':
+ self.no_home_for_book.extend([entry[0] for entry in self.duplicate_book_list])
+ else:
+ if self.ask_about_inserting_epubs():
+ # Find homes for the epub decrypted formats in existing calibre library entries.
+ self.process_epub_formats()
+ else:
+ print (_('{} - User opted not to try to insert EPUB formats').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION))
+ self.no_home_for_book.extend([entry[0] for entry in self.duplicate_book_list])
+
+ print (_('{} - wrapping up results.').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION))
+ self.wrap_up_results()
+ remove_dir(self.tdir)
+ return
+
+ def show_help(self):
+ '''
+ Extract on demand the help file resource
+ '''
+ def get_help_file_resource():
+ # We will write the help file out every time, in case the user upgrades the plugin zip
+ # and there is a newer help file contained within it.
+ file_path = os.path.join(config_dir, 'plugins', HELPFILE_NAME)
+ file_data = self.load_resources(HELPFILE_NAME)[HELPFILE_NAME]
+ with open(file_path,'w') as f:
+ f.write(file_data)
+ return file_path
+ url = 'file:///' + get_help_file_resource()
+ open_url(QUrl(url))
+
+ def build_book_list(self):
+ '''
+ Connect to Kobo db and get titles.
+ '''
+ return self.library.books
+
+ def get_decrypted_kobo_books(self, book):
+ '''
+ This method is a call-back function used by DecryptAddProgressDialog in dialogs.py to decrypt Kobo books
+
+ :param book: A KoboBook object that is to be decrypted.
+ '''
+ print (_('{0} - Decrypting {1}').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION, book.title))
+ decrypted = self.decryptBook(book)
+ if decrypted['success']:
+ # Build a list of calibre "book maps" for calibre's add_book function.
+ mi = get_metadata(decrypted['fileobj'], 'epub')
+ bookmap = {'EPUB':decrypted['fileobj'].name}
+ self.books_to_add.append((mi, bookmap))
+ else:
+ # Book is probably still encrypted.
+ print (_('{0} - Couldn\'t decrypt {1}').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION, book.title))
+ self.decryption_errors.append((book.title, _('decryption errors')))
+ return False
+ return True
+
+ def add_new_books(self, books_to_add):
+ '''
+ This method is a call-back function used by DecryptAddProgressDialog in dialogs.py to add books to calibre
+ (It's set up to handle multiple books, but will only be fed books one at a time by DecryptAddProgressDialog)
+
+ :param books_to_add: List of calibre bookmaps (created in get_decrypted_kobo_books)
+ '''
+ added = self.db.add_books(books_to_add, add_duplicates=False, run_hooks=False)
+ if len(added[0]):
+ # Record the id(s) that got added
+ for id in added[0]:
+ print (_('{0} - Added {1}').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION, books_to_add[0][0].title))
+ self.ids_of_new_books.append((id, books_to_add[0][0]))
+ if len(added[1]):
+ # Build a list of details about the books that didn't get added because duplicate were detected.
+ for mi, map in added[1]:
+ print (_('{0} - {1} already exists. Will try to add format later.').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION, mi.title))
+ self.duplicate_book_list.append((mi, map['EPUB'], _('duplicate detected')))
+ return False
+ return True
+
+ def add_epub_format(self, book_id, mi, path):
+ '''
+ This method is a call-back function used by AddEpubFormatsProgressDialog in dialogs.py
+
+ :param book_id: calibre ID of the book to add the encrypted epub to.
+ :param mi: calibre metadata object
+ :param path: path to the decrypted epub (temp file)
+ '''
+ if self.db.add_format(book_id, 'EPUB', path, replace=False, run_hooks=False):
+ self.successful_format_adds.append((book_id, mi))
+ print (_('{0} - Successfully added EPUB format to existing {1}').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION, mi.title))
+ return True
+ # we really shouldn't get here.
+ print (_('{0} - Error adding EPUB format to existing {1}. This really shouldn\'t happen.').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION, mi.title))
+ self.no_home_for_book.append(mi)
+ return False
+
+ def process_epub_formats(self):
+ '''
+ Ask the user if they want to try to find homes for those books that already had an entry in calibre
+ '''
+ for book in self.duplicate_book_list:
+ mi, tmp_file = book[0], book[1]
+ dup_ids = self.db.find_identical_books(mi)
+ home_id = self.find_a_home(dup_ids)
+ if home_id is not None:
+ # Found an epub-free duplicate to add the epub to.
+ # build a list for the add_epub_format method to use.
+ self.formats_to_add.append((home_id, mi, tmp_file))
+ else:
+ self.no_home_for_book.append(mi)
+ # If we found homes for decrypted epubs in existing calibre entries, feed the list of decrypted book
+ # details and the callback function (self.add_epub_format) to the ProgressDialog dispatcher.
+ if self.formats_to_add:
+ d = AddEpubFormatsProgressDialog(self.gui, self.formats_to_add, self.add_epub_format)
+ if d.wasCanceled():
+ print (_('{} - "Insert formats" canceled by user.').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION))
+ self.add_formats_cancelled = True
+ return
+ #return
+ return
+
+ def wrap_up_results(self):
+ '''
+ Present the results
+ '''
+ caption = PLUGIN_NAME + ' v' + PLUGIN_VERSION
+ # Refresh the gui and highlight new entries/modified entries.
+ if len(self.ids_of_new_books) or len(self.successful_format_adds):
+ self.refresh_gui_lib()
+
+ msg, log = self.build_report()
+
+ sd = ResultsSummaryDialog(self.gui, caption, msg, log)
+ sd.exec_()
+ return
+
+ def ask_about_inserting_epubs(self):
+ '''
+ Build question dialog with details about kobo books
+ that couldn't be added to calibre as new books.
+ '''
+ ''' Terisa: Improve the message
+ '''
+ caption = PLUGIN_NAME + ' v' + PLUGIN_VERSION
+ plural = format_plural(len(self.ids_of_new_books))
+ det_msg = ''
+ if self.count > 1:
+ msg = _('<p><b>{0}</b> EPUB{2} successfully added to library.<br /><br /><b>{1}</b> ').format(len(self.ids_of_new_books), len(self.duplicate_book_list), plural)
+ msg += _('not added because books with the same title/author were detected.<br /><br />Would you like to try and add the EPUB format{0}').format(plural)
+ msg += _(' to those existing entries?<br /><br />NOTE: no pre-existing EPUBs will be overwritten.')
+ for entry in self.duplicate_book_list:
+ det_msg += _('{0} -- not added because of {1} in your library.\n\n').format(entry[0].title, entry[2])
+ else:
+ msg = _('<p><b>{0}</b> -- not added because of {1} in your library.<br /><br />').format(self.duplicate_book_list[0][0].title, self.duplicate_book_list[0][2])
+ msg += _('Would you like to try and add the EPUB format to an available calibre duplicate?<br /><br />')
+ msg += _('NOTE: no pre-existing EPUB will be overwritten.')
+
+ return question_dialog(self.gui, caption, msg, det_msg)
+
+ def find_a_home(self, ids):
+ '''
+ Find the ID of the first EPUB-Free duplicate available
+
+ :param ids: List of calibre IDs that might serve as a home.
+ '''
+ for id in ids:
+ # Find the first entry that matches the incoming book that doesn't have an EPUB format.
+ if not self.db.has_format(id, 'EPUB'):
+ return id
+ break
+ return None
+
+ def refresh_gui_lib(self):
+ '''
+ Update the GUI; highlight the books that were added/modified
+ '''
+ if self.current_idx.isValid():
+ self.gui.library_view.model().current_changed(self.current_idx, self.current_idx)
+ new_entries = [id for id, mi in self.ids_of_new_books]
+ if new_entries:
+ self.gui.library_view.model().db.data.books_added(new_entries)
+ self.gui.library_view.model().books_added(len(new_entries))
+ new_entries.extend([id for id, mi in self.successful_format_adds])
+ self.gui.db_images.reset()
+ self.gui.tags_view.recount()
+ self.gui.library_view.model().set_highlight_only(True)
+ self.gui.library_view.select_rows(new_entries)
+ return
+
+ def decryptBook(self, book):
+ '''
+ Decrypt Kobo book
+
+ :param book: obok file object
+ '''
+ result = {}
+ result['success'] = False
+ result['fileobj'] = None
+
+ zin = zipfile.ZipFile(book.filename, 'r')
+ #print ('Kobo library filename: {0}'.format(book.filename))
+ for userkey in self.userkeys:
+ print (_('Trying key: '), userkey.encode('hex_codec'))
+ check = True
+ try:
+ fileout = PersistentTemporaryFile('.epub', dir=self.tdir)
+ #print ('Temp file: {0}'.format(fileout.name))
+ # modify the output file to be compressed by default
+ zout = zipfile.ZipFile(fileout.name, "w", zipfile.ZIP_DEFLATED)
+ # ensure that the mimetype file is the first written to the epub container
+ # and is stored with no compression
+ members = zin.namelist();
+ try:
+ members.remove('mimetype')
+ except Exception:
+ pass
+ zout.writestr('mimetype', 'application/epub+zip', zipfile.ZIP_STORED)
+ # end of mimetype mod
+ for filename in members:
+ contents = zin.read(filename)
+ if filename in book.encryptedfiles:
+ file = book.encryptedfiles[filename]
+ contents = file.decrypt(userkey, contents)
+ # Parse failures mean the key is probably wrong.
+ if check:
+ check = not file.check(contents)
+ zout.writestr(filename, contents)
+ zout.close()
+ zin.close()
+ result['success'] = True
+ result['fileobj'] = fileout
+ print ('Success!')
+ return result
+ except ValueError:
+ print (_('Decryption failed, trying next key.'))
+ zout.close()
+ continue
+ except Exception:
+ print (_('Unknown Error decrypting, trying next key..'))
+ zout.close()
+ continue
+ result['fileobj'] = book.filename
+ zin.close()
+ return result
+
+ def build_report(self):
+ log = ''
+ processed = len(self.ids_of_new_books) + len(self.successful_format_adds)
+
+ if processed == self.count:
+ if self.count > 1:
+ msg = _('<p>All selected Kobo books added as new calibre books or inserted into existing calibre ebooks.<br /><br />No issues.')
+ else:
+ # Single book ... don't get fancy.
+ title = self.ids_of_new_books[0][1].title if self.ids_of_new_books else self.successful_format_adds[0][1].title
+ msg = _('<p>{0} successfully added.').format(title)
+ return (msg, log)
+ else:
+ if self.count != 1:
+ msg = _('<p>Not all selected Kobo books made it into calibre.<br /><br />View report for details.')
+ log += _('<p><b>Total attempted:</b> {}</p>\n').format(self.count)
+ log += _('<p><b>Decryption errors:</b> {}</p>\n').format(len(self.decryption_errors))
+ if self.decryption_errors:
+ log += '<ul>\n'
+ for title, reason in self.decryption_errors:
+ log += '<li>{}</li>\n'.format(title)
+ log += '</ul>\n'
+ log += _('<p><b>New Books created:</b> {}</p>\n').format(len(self.ids_of_new_books))
+ if self.ids_of_new_books:
+ log += '<ul>\n'
+ for id, mi in self.ids_of_new_books:
+ log += '<li>{}</li>\n'.format(mi.title)
+ log += '</ul>\n'
+ if self.add_books_cancelled:
+ log += _('<p><b>Duplicates that weren\'t added:</b> {}</p>\n').format(len(self.duplicate_book_list))
+ if self.duplicate_book_list:
+ log += '<ul>\n'
+ for book in self.duplicate_book_list:
+ log += '<li>{}</li>\n'.format(book[0].title)
+ log += '</ul>\n'
+ cancelled_count = self.count - (len(self.decryption_errors) + len(self.ids_of_new_books) + len(self.duplicate_book_list))
+ if cancelled_count > 0:
+ log += _('<p><b>Book imports cancelled by user:</b> {}</p>\n').format(cancelled_count)
+ return (msg, log)
+ log += _('<p><b>New EPUB formats inserted in existing calibre books:</b> {0}</p>\n').format(len(self.successful_format_adds))
+ if self.successful_format_adds:
+ log += '<ul>\n'
+ for id, mi in self.successful_format_adds:
+ log += '<li>{}</li>\n'.format(mi.title)
+ log += '</ul>\n'
+ log += _('<p><b>EPUB formats NOT inserted into existing calibre books:</b> {}<br />\n').format(len(self.no_home_for_book))
+ log += _('(Either because the user <i>chose</i> not to insert them, or because all duplicates already had an EPUB format)')
+ if self.no_home_for_book:
+ log += '<ul>\n'
+ for mi in self.no_home_for_book:
+ log += '<li>{}</li>\n'.format(mi.title)
+ log += '</ul>\n'
+ if self.add_formats_cancelled:
+ cancelled_count = self.count - (len(self.decryption_errors) + len(self.ids_of_new_books) + len(self.successful_format_adds) + len(self.no_home_for_book))
+ if cancelled_count > 0:
+ log += _('<p><b>Format imports cancelled by user:</b> {}</p>\n').format(cancelled_count)
+ return (msg, log)
+ else:
+
+ # Single book ... don't get fancy.
+ if self.ids_of_new_books:
+ title = self.ids_of_new_books[0][1].title
+ elif self.successful_format_adds:
+ title = self.successful_format_adds[0][1].title
+ elif self.no_home_for_book:
+ title = self.no_home_for_book[0].title
+ elif self.decryption_errors:
+ title = self.decryption_errors[0][0]
+ else:
+ title = _('Unknown Book Title')
+ if self.decryption_errors:
+ reason = _('it couldn\'t be decrypted.')
+ elif self.no_home_for_book:
+ reason = _('user CHOSE not to insert the new EPUB format, or all existing calibre entries HAD an EPUB format already.')
+ else:
+ reason = _('of unknown reasons. Gosh I\'m embarrassed!')
+ msg = _('<p>{0} not added because {1}').format(title, reason)
+ return (msg, log)
+
diff --git a/Obok_calibre_plugin/obok_plugin/common_utils.py b/Obok_calibre_plugin/obok_plugin/common_utils.py
new file mode 100644
index 0000000..964753f
--- /dev/null
+++ b/Obok_calibre_plugin/obok_plugin/common_utils.py
@@ -0,0 +1,589 @@
+#!/usr/bin/env python
+# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
+from __future__ import (unicode_literals, division, absolute_import,
+ print_function)
+
+__license__ = 'GPL v3'
+__copyright__ = '2012, David Forrester <[email protected]>'
+__docformat__ = 'restructuredtext en'
+
+import os, time, re, sys
+try:
+ from PyQt5.Qt import (Qt, QIcon, QPixmap, QLabel, QDialog, QHBoxLayout, QProgressBar,
+ QTableWidgetItem, QFont, QLineEdit, QComboBox,
+ QVBoxLayout, QDialogButtonBox, QStyledItemDelegate, QDateTime,
+ QRegExpValidator, QRegExp, QDate, QDateEdit)
+except ImportError:
+ from PyQt4.Qt import (Qt, QIcon, QPixmap, QLabel, QDialog, QHBoxLayout, QProgressBar,
+ QTableWidgetItem, QFont, QLineEdit, QComboBox,
+ QVBoxLayout, QDialogButtonBox, QStyledItemDelegate, QDateTime,
+ QRegExpValidator, QRegExp, QDate, QDateEdit)
+
+from calibre.constants import iswindows, filesystem_encoding, DEBUG
+from calibre.gui2 import gprefs, error_dialog, UNDEFINED_QDATETIME, Application
+from calibre.gui2.actions import menu_action_unique_name
+from calibre.gui2.keyboard import ShortcutConfig
+from calibre.utils.config import config_dir, tweaks
+from calibre.utils.date import now, format_date, qt_to_dt, UNDEFINED_DATE, as_local_time
+from calibre import prints
+
+# 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 = {}
+
+BASE_TIME = None
+def debug_print(*args):
+ global BASE_TIME
+ if BASE_TIME is None:
+ BASE_TIME = time.time()
+ if DEBUG:
+ prints('DEBUG: %6.1f'%(time.time()-BASE_TIME), *args)
+
+
+try:
+ debug_print("obok::common_utils.py - loading translations")
+ load_translations()
+except NameError:
+ debug_print("obok::common_utils.py - exception when loading translations")
+ pass # load_translations() added in calibre 1.9
+
+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 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):
+ QHBoxLayout.__init__(self)
+ self.title_image_label = QLabel(parent)
+ self.update_title_icon(icon_name)
+ self.addWidget(self.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)
+
+ # Add hyperlink to a help file at the right. We will replace the correct name when it is clicked.
+ help_label = QLabel(('<a href="http://www.foo.com/">{0}</a>').format(_("Help")), parent)
+ help_label.setTextInteractionFlags(Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard)
+ help_label.setAlignment(Qt.AlignRight)
+ help_label.linkActivated.connect(parent.help_link_activated)
+ self.addWidget(help_label)
+
+ def update_title_icon(self, icon_name):
+ pixmap = get_pixmap(icon_name)
+ if pixmap is None:
+ error_dialog(self.parent(), _("Restart required"),
+ _("Title image not found - you must restart Calibre before using this plugin!"), show=True)
+ else:
+ self.title_image_label.setPixmap(pixmap)
+ self.title_image_label.setMaximumSize(32, 32)
+ self.title_image_label.setScaledContents(True)
+
+
+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)
+ self.help_anchor = ''
+
+ def resize_dialog(self):
+ if self.geom is None:
+ self.resize(self.sizeHint())
+ else:
+ self.restoreGeometry(self.geom)
+
+ def dialog_closing(self, result):
+ geom = bytearray(self.saveGeometry())
+ gprefs[self.unique_pref_name] = geom
+
+ def help_link_activated(self, url):
+ self.plugin_action.show_help(anchor=self.help_anchor)
+
+
+class ReadOnlyTableWidgetItem(QTableWidgetItem):
+
+ def __init__(self, text):
+ if text is None:
+ text = ''
+ QTableWidgetItem.__init__(self, text, QTableWidgetItem.UserType)
+ self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
+
+class RatingTableWidgetItem(QTableWidgetItem):
+
+ def __init__(self, rating, is_read_only=False):
+ QTableWidgetItem.__init__(self, '', 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, fmt=None):
+# debug_print("DateTableWidgetItem:__init__ - date_read=", date_read)
+ if date_read is None or date_read == UNDEFINED_DATE and default_to_today:
+ date_read = now()
+ if is_read_only:
+ QTableWidgetItem.__init__(self, format_date(date_read, fmt), QTableWidgetItem.UserType)
+ self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
+ self.setData(Qt.DisplayRole, QDateTime(date_read))
+ else:
+ QTableWidgetItem.__init__(self, '', QTableWidgetItem.UserType)
+ self.setData(Qt.DisplayRole, QDateTime(date_read))
+
+from calibre.gui2.library.delegates import DateDelegate as _DateDelegate
+class DateDelegate(_DateDelegate):
+ '''
+ 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, fmt='dd MMM yyyy', default_to_today=True):
+ _DateDelegate.__init__(self, parent)
+ self.format = fmt
+ self.default_to_today = default_to_today
+
+# 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:
+ if self.default_to_today:
+ val = self.default_date
+ else:
+ val = UNDEFINED_QDATETIME
+ 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 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 NumericLineEdit(QLineEdit):
+ '''
+ Allows a numeric value up to two decimal places, or an integer
+ '''
+ def __init__(self, *args):
+ QLineEdit.__init__(self, *args)
+ self.setValidator(QRegExpValidator(QRegExp(r'(^\d*\.[\d]{1,2}$)|(^[1-9]\d*[\.]$)'), self))
+
+
+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 KeyComboBox(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 in sorted(self.values.keys()):
+ idx = idx + 1
+ self.addItem(key)
+ if key == selected_key:
+ selected_idx = idx
+ self.setCurrentIndex(selected_idx)
+
+ def selected_key(self):
+ for key, value in self.values.iteritems():
+ if key == 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 = list(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, '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 ProgressBar(QDialog):
+ def __init__(self, parent=None, max_items=100, window_title='Progress Bar',
+ label='Label goes here', on_top=False):
+ if on_top:
+ QDialog.__init__(self, parent=parent, flags=Qt.WindowStaysOnTopHint)
+ else:
+ QDialog.__init__(self, parent=parent)
+ self.application = Application
+ self.setWindowTitle(window_title)
+ self.l = QVBoxLayout(self)
+ self.setLayout(self.l)
+
+ self.label = QLabel(label)
+ self.label.setAlignment(Qt.AlignHCenter)
+ self.l.addWidget(self.label)
+
+ self.progressBar = QProgressBar(self)
+ self.progressBar.setRange(0, max_items)
+ self.progressBar.setValue(0)
+ self.l.addWidget(self.progressBar)
+
+ def increment(self):
+ self.progressBar.setValue(self.progressBar.value() + 1)
+ self.refresh()
+
+ def refresh(self):
+ self.application.processEvents()
+
+ def set_label(self, value):
+ self.label.setText(value)
+ self.refresh()
+
+ def set_maximum(self, value):
+ self.progressBar.setMaximum(value)
+ self.refresh()
+
+ def set_value(self, value):
+ self.progressBar.setValue(value)
+ self.refresh()
+
+def convert_kobo_date(kobo_date):
+ from calibre.utils.date import utc_tz
+
+ try:
+ converted_date = datetime.strptime(kobo_date, "%Y-%m-%dT%H:%M:%S.%f")
+ converted_date = datetime.strptime(kobo_date[0:19], "%Y-%m-%dT%H:%M:%S")
+ converted_date = converted_date.replace(tzinfo=utc_tz)
+# debug_print("convert_kobo_date - '%Y-%m-%dT%H:%M:%S.%f' - kobo_date={0}'".format(kobo_date))
+ except:
+ try:
+ converted_date = datetime.strptime(kobo_date, "%Y-%m-%dT%H:%M:%S%+00:00")
+# debug_print("convert_kobo_date - '%Y-%m-%dT%H:%M:%S+00:00' - kobo_date=%s' - kobo_date={0}'".format(kobo_date))
+ except:
+ try:
+ converted_date = datetime.strptime(kobo_date.split('+')[0], "%Y-%m-%dT%H:%M:%S")
+ converted_date = converted_date.replace(tzinfo=utc_tz)
+# debug_print("convert_kobo_date - '%Y-%m-%dT%H:%M:%S' - kobo_date={0}'".format(kobo_date))
+ except:
+ try:
+ converted_date = datetime.strptime(kobo_date.split('+')[0], "%Y-%m-%d")
+ converted_date = converted_date.replace(tzinfo=utc_tz)
+# debug_print("convert_kobo_date - '%Y-%m-%d' - kobo_date={0}'".format(kobo_date))
+ except:
+ try:
+ from calibre.utils.date import parse_date
+ converted_date = parse_date(kobo_date, assume_utc=True)
+# debug_print("convert_kobo_date - parse_date - kobo_date=%s' - kobo_date={0}'".format(kobo_date))
+ except:
+# try:
+# converted_date = time.gmtime(os.path.getctime(self.path))
+# debug_print("convert_kobo_date - time.gmtime(os.path.getctime(self.path)) - kobo_date={0}'".format(kobo_date))
+# except:
+ converted_date = time.gmtime()
+ debug_print("convert_kobo_date - time.gmtime() - kobo_date={0}'".format(kobo_date))
+ return converted_date
diff --git a/Obok_calibre_plugin/obok_plugin/config.py b/Obok_calibre_plugin/obok_plugin/config.py
new file mode 100644
index 0000000..c4e470e
--- /dev/null
+++ b/Obok_calibre_plugin/obok_plugin/config.py
@@ -0,0 +1,40 @@
+# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
+from __future__ import (unicode_literals, division, absolute_import,
+ print_function)
+
+try:
+ from PyQt5.Qt import (QWidget, QLabel, QVBoxLayout, QHBoxLayout, QComboBox)
+except ImportError:
+ from PyQt4.Qt import (QWidget, QLabel, QVBoxLayout, QHBoxLayout, QComboBox)
+
+from calibre.utils.config import JSONConfig, config_dir
+
+plugin_prefs = JSONConfig('plugins/obok_dedrm_prefs')
+plugin_prefs.defaults['finding_homes_for_formats'] = 'Ask'
+
+from calibre_plugins.obok_dedrm.utilities import (debug_print)
+try:
+ debug_print("obok::config.py - loading translations")
+ load_translations()
+except NameError:
+ debug_print("obok::config.py - exception when loading translations")
+ pass # load_translations() added in calibre 1.9
+
+class ConfigWidget(QWidget):
+ def __init__(self, plugin_action):
+ QWidget.__init__(self)
+ self.plugin_action = plugin_action
+ layout = QVBoxLayout(self)
+ self.setLayout(layout)
+
+ combo_label = QLabel(_('When should Obok try to insert EPUBs into existing calibre entries?'), self)
+ layout.addWidget(combo_label)
+ self.find_homes = QComboBox()
+ self.find_homes.setToolTip(_('<p>Default behavior when duplicates are detected. None of the choices will cause calibre ebooks to be overwritten'))
+ layout.addWidget(self.find_homes)
+ self.find_homes.addItems([_('Ask'), _('Always'), _('Never')])
+ index = self.find_homes.findText(plugin_prefs['finding_homes_for_formats'])
+ self.find_homes.setCurrentIndex(index)
+
+ def save_settings(self):
+ plugin_prefs['finding_homes_for_formats'] = unicode(self.find_homes.currentText())
diff --git a/Obok_calibre_plugin/obok_plugin/default.po b/Obok_calibre_plugin/obok_plugin/default.po
new file mode 100644
index 0000000..2b73c84
--- /dev/null
+++ b/Obok_calibre_plugin/obok_plugin/default.po
@@ -0,0 +1,335 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-17 12:51+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <[email protected]>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=CHARSET\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:80
+msgid ""
+"<p>No books found in Kobo Library\n"
+"Are you sure it's installed\\configured\\synchronized?"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:87
+msgid "Legacy key found: "
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:93
+msgid "Trouble retrieving keys with newer obok method."
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:97
+msgid "Found {0} possible keys to try."
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:99
+msgid "<p>No userkeys found to decrypt books with. No point in proceeding."
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:115
+msgid "{} - Decryption canceled by user."
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:135
+msgid "{} - \"Add books\" canceled by user."
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:137
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:156
+msgid "{} - wrapping up results."
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:153
+msgid "{} - User opted not to try to insert EPUB formats"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:188
+msgid "{0} - Decrypting {1}"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:197
+msgid "{0} - Couldn't decrypt {1}"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:198
+msgid "decryption errors"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:213
+msgid "{0} - Added {1}"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:218
+msgid "{0} - {1} already exists. Will try to add format later."
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:219
+msgid "duplicate detected"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:233
+msgid "{0} - Successfully added EPUB format to existing {1}"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:236
+msgid ""
+"{0} - Error adding EPUB format to existing {1}. This really shouldn't happen."
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:259
+msgid "{} - \"Insert formats\" canceled by user."
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:291
+msgid ""
+"<p><b>{0}</b> EPUB{2} successfully added to library.<br /><br /><b>{1}</b> "
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:292
+msgid ""
+"not added because books with the same title/author were detected.<br /><br /"
+">Would you like to try and add the EPUB format{0}"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:293
+msgid ""
+" to those existing entries?<br /><br />NOTE: no pre-existing EPUBs will be "
+"overwritten."
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:295
+msgid ""
+"{0} -- not added because of {1} in your library.\n"
+"\n"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:297
+msgid "<p><b>{0}</b> -- not added because of {1} in your library.<br /><br />"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:298
+msgid ""
+"Would you like to try and add the EPUB format to an available calibre "
+"duplicate?<br /><br />"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:299
+msgid "NOTE: no pre-existing EPUB will be overwritten."
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:346
+msgid "Trying key: "
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:378
+msgid "Decryption failed, trying next key."
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:382
+msgid "Unknown Error decrypting, trying next key.."
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:395
+msgid ""
+"<p>All selected Kobo books added as new calibre books or inserted into "
+"existing calibre ebooks.<br /><br />No issues."
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:399
+msgid "<p>{0} successfully added."
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:403
+msgid ""
+"<p>Not all selected Kobo books made it into calibre.<br /><br />View report "
+"for details."
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:404
+msgid "<p><b>Total attempted:</b> {}</p>\n"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:405
+msgid "<p><b>Decryption errors:</b> {}</p>\n"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:411
+msgid "<p><b>New Books created:</b> {}</p>\n"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:418
+msgid "<p><b>Duplicates that weren't added:</b> {}</p>\n"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:426
+msgid "<p><b>Book imports cancelled by user:</b> {}</p>\n"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:428
+msgid ""
+"<p><b>New EPUB formats inserted in existing calibre books:</b> {0}</p>\n"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:434
+msgid ""
+"<p><b>EPUB formats NOT inserted into existing calibre books:</b> {}<br />\n"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:435
+msgid ""
+"(Either because the user <i>chose</i> not to insert them, or because all "
+"duplicates already had an EPUB format)"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:444
+msgid "<p><b>Format imports cancelled by user:</b> {}</p>\n"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:458
+msgid "Unknown Book Title"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:460
+msgid "it couldn't be decrypted."
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:462
+msgid ""
+"user CHOSE not to insert the new EPUB format, or all existing calibre "
+"entries HAD an EPUB format already."
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:464
+msgid "of unknown reasons. Gosh I'm embarrassed!"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:465
+msgid "<p>{0} not added because {1}"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:226
+msgid "Help"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:235
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\utilities.py:214
+msgid "Restart required"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:236
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\utilities.py:215
+msgid ""
+"Title image not found - you must restart Calibre before using this plugin!"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:322
+msgid "Undefined"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:30
+msgid "When should Obok try to insert EPUBs into existing calibre entries?"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:33
+msgid ""
+"<p>Default behavior when duplicates are detected. None of the choices will "
+"cause calibre ebooks to be overwritten"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:35
+msgid "Ask"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:35
+msgid "Always"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:35
+msgid "Never"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:60
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\utilities.py:150
+msgid " v"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:65
+msgid "Obok DeDRM"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:68
+msgid "<a href=\"http://www.foo.com/\">Help</a>"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:89
+msgid "Select All"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:90
+msgid "Select all books to add them to the calibre library."
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:92
+msgid "All with DRM"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:93
+msgid "Select all books with DRM."
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:95
+msgid "All DRM free"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:96
+msgid "Select all books without DRM."
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:146
+msgid "Title"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:146
+msgid "Author"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:146
+msgid "Series"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:369
+msgid "Copy to clipboard"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:397
+msgid "View Report"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\__init__.py:21
+msgid "Removes DRM from Kobo kepubs and adds them to the library."
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:162
+msgid "AES improper key used"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:167
+msgid "Failed to initialize AES key"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:175
+msgid "AES decryption failed"
+msgstr ""
diff --git a/Obok_calibre_plugin/obok_plugin/dialogs.py b/Obok_calibre_plugin/obok_plugin/dialogs.py
new file mode 100644
index 0000000..85abfaf
--- /dev/null
+++ b/Obok_calibre_plugin/obok_plugin/dialogs.py
@@ -0,0 +1,455 @@
+# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
+from __future__ import (unicode_literals, division, absolute_import,
+ print_function)
+
+__license__ = 'GPL v3'
+__docformat__ = 'restructuredtext en'
+
+TEXT_DRM_FREE = ' (*: drm - free)'
+LAB_DRM_FREE = '* : drm - free'
+
+try:
+ from PyQt5.Qt import (Qt, QVBoxLayout, QLabel, QApplication, QGroupBox,
+ QDialogButtonBox, QHBoxLayout, QTextBrowser, QProgressDialog,
+ QTimer, QSize, QDialog, QIcon, QTableWidget, QTableWidgetItem)
+except ImportError:
+ from PyQt4.Qt import (Qt, QVBoxLayout, QLabel, QApplication, QGroupBox,
+ QDialogButtonBox, QHBoxLayout, QTextBrowser, QProgressDialog,
+ QTimer, QSize, QDialog, QIcon, QTableWidget, QTableWidgetItem)
+
+try:
+ from PyQt5.QtWidgets import (QListWidget, QAbstractItemView)
+except ImportError:
+ from PyQt4.QtGui import (QListWidget, QAbstractItemView)
+
+from calibre.gui2 import gprefs, warning_dialog, error_dialog
+from calibre.gui2.dialogs.message_box import MessageBox
+
+#from calibre.ptempfile import remove_dir
+
+from calibre_plugins.obok_dedrm.utilities import (SizePersistedDialog, ImageTitleLayout,
+ showErrorDlg, get_icon, convert_qvariant, debug_print
+ )
+from calibre_plugins.obok_dedrm.__init__ import (PLUGIN_NAME,
+ PLUGIN_SAFE_NAME, PLUGIN_VERSION, PLUGIN_DESCRIPTION)
+
+try:
+ debug_print("obok::dialogs.py - loading translations")
+ load_translations()
+except NameError:
+ debug_print("obok::dialogs.py - exception when loading translations")
+ pass # load_translations() added in calibre 1.9
+
+class SelectionDialog(SizePersistedDialog):
+ '''
+ Dialog to select the kobo books to decrypt
+ '''
+ def __init__(self, gui, interface_action, books):
+ '''
+ :param gui: Parent gui
+ :param interface_action: InterfaceActionObject (InterfacePluginAction class from action.py)
+ :param books: list of Kobo book
+ '''
+
+ self.books = books
+ self.gui = gui
+ self.interface_action = interface_action
+ self.books = books
+
+ SizePersistedDialog.__init__(self, gui, PLUGIN_NAME + 'plugin:selections dialog')
+ self.setWindowTitle(_(PLUGIN_NAME + ' v' + PLUGIN_VERSION))
+ self.setMinimumWidth(300)
+ self.setMinimumHeight(300)
+ layout = QVBoxLayout(self)
+ self.setLayout(layout)
+ title_layout = ImageTitleLayout(self, 'images/obok.png', _('Obok DeDRM'))
+ layout.addLayout(title_layout)
+
+ help_label = QLabel(_('<a href="http://www.foo.com/">Help</a>'), self)
+ help_label.setTextInteractionFlags(Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard)
+ help_label.setAlignment(Qt.AlignRight)
+ help_label.linkActivated.connect(self._help_link_activated)
+ title_layout.addWidget(help_label)
+ title_layout.setAlignment(Qt.AlignTop)
+
+ layout.addSpacing(5)
+ main_layout = QHBoxLayout()
+ layout.addLayout(main_layout)
+# self.listy = QListWidget()
+# self.listy.setSelectionMode(QAbstractItemView.ExtendedSelection)
+# main_layout.addWidget(self.listy)
+# self.listy.addItems(books)
+ self.books_table = BookListTableWidget(self)
+ main_layout.addWidget(self.books_table)
+
+ layout.addSpacing(10)
+ button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
+ button_box.accepted.connect(self._ok_clicked)
+ button_box.rejected.connect(self.reject)
+ self.select_all_button = button_box.addButton(_("Select All"), QDialogButtonBox.ResetRole)
+ self.select_all_button.setToolTip(_("Select all books to add them to the calibre library."))
+ self.select_all_button.clicked.connect(self._select_all_clicked)
+ self.select_drm_button = button_box.addButton(_("All with DRM"), QDialogButtonBox.ResetRole)
+ self.select_drm_button.setToolTip(_("Select all books with DRM."))
+ self.select_drm_button.clicked.connect(self._select_drm_clicked)
+ self.select_free_button = button_box.addButton(_("All DRM free"), QDialogButtonBox.ResetRole)
+ self.select_free_button.setToolTip(_("Select all books without DRM."))
+ self.select_free_button.clicked.connect(self._select_free_clicked)
+ layout.addWidget(button_box)
+
+ # Cause our dialog size to be restored from prefs or created on first usage
+ self.resize_dialog()
+ self.books_table.populate_table(self.books)
+
+ def _select_all_clicked(self):
+ self.books_table.select_all()
+
+ def _select_drm_clicked(self):
+ self.books_table.select_drm(True)
+
+ def _select_free_clicked(self):
+ self.books_table.select_drm(False)
+
+ def _help_link_activated(self, url):
+ '''
+ :param url: Dummy url to pass to the show_help method of the InterfacePluginAction class
+ '''
+ self.interface_action.show_help()
+
+ def _ok_clicked(self):
+ '''
+ Build an index of the selected titles
+ '''
+ if len(self.books_table.selectedItems()):
+ self.accept()
+ else:
+ msg = 'You must make a selection!'
+ showErrorDlg(msg, self)
+
+ def getBooks(self):
+ '''
+ Method to return the selected books
+ '''
+ return self.books_table.get_books()
+
+
+class BookListTableWidget(QTableWidget):
+
+ def __init__(self, parent):
+ QTableWidget.__init__(self, parent)
+ self.setSelectionBehavior(QAbstractItemView.SelectRows)
+
+ def populate_table(self, books):
+ self.clear()
+ self.setAlternatingRowColors(True)
+ self.setRowCount(len(books))
+ header_labels = ['DRM', _('Title'), _('Author'), _('Series'), 'book_id']
+ self.setColumnCount(len(header_labels))
+ self.setHorizontalHeaderLabels(header_labels)
+ self.verticalHeader().setDefaultSectionSize(24)
+ self.horizontalHeader().setStretchLastSection(True)
+
+ self.books = {}
+ for row, book in enumerate(books):
+ self.populate_table_row(row, book)
+ self.books[row] = book
+
+ self.setSortingEnabled(False)
+ self.resizeColumnsToContents()
+ self.setMinimumColumnWidth(1, 100)
+ self.setMinimumColumnWidth(2, 100)
+ self.setMinimumSize(300, 0)
+ if len(books) > 0:
+ self.selectRow(0)
+ self.hideColumn(4)
+ self.setSortingEnabled(True)
+
+ def setMinimumColumnWidth(self, col, minimum):
+ if self.columnWidth(col) < minimum:
+ self.setColumnWidth(col, minimum)
+
+ def populate_table_row(self, row, book):
+ if book.has_drm:
+ icon = get_icon('drm-locked.png')
+ val = 1
+ else:
+ icon = get_icon('drm-unlocked.png')
+ val = 0
+
+ status_cell = IconWidgetItem(None, icon, val)
+ status_cell.setData(Qt.UserRole, val)
+ self.setItem(row, 0, status_cell)
+ self.setItem(row, 1, ReadOnlyTableWidgetItem(book.title))
+ self.setItem(row, 2, AuthorTableWidgetItem(book.author, book.author))
+ self.setItem(row, 3, SeriesTableWidgetItem(book.series, book.series_index))
+ self.setItem(row, 4, NumericTableWidgetItem(row))
+
+ def get_books(self):
+# debug_print("BookListTableWidget:get_books - self.books:", self.books)
+ books = []
+ if len(self.selectedItems()):
+ for row in range(self.rowCount()):
+# debug_print("BookListTableWidget:get_books - row:", row)
+ if self.item(row, 0).isSelected():
+ book_num = convert_qvariant(self.item(row, 4).data(Qt.DisplayRole))
+ debug_print("BookListTableWidget:get_books - book_num:", book_num)
+ book = self.books[book_num]
+ debug_print("BookListTableWidget:get_books - book:", book.title)
+ books.append(book)
+ return books
+
+ def select_all(self):
+ self .selectAll()
+
+ def select_drm(self, has_drm):
+ self.clearSelection()
+ current_selection_mode = self.selectionMode()
+ self.setSelectionMode(QAbstractItemView.MultiSelection)
+ for row in range(self.rowCount()):
+# debug_print("BookListTableWidget:select_drm - row:", row)
+ if convert_qvariant(self.item(row, 0).data(Qt.UserRole)) == 1:
+# debug_print("BookListTableWidget:select_drm - has DRM:", row)
+ if has_drm:
+ self.selectRow(row)
+ else:
+# debug_print("BookListTableWidget:select_drm - DRM free:", row)
+ if not has_drm:
+ self.selectRow(row)
+ self.setSelectionMode(current_selection_mode)
+
+
+class DecryptAddProgressDialog(QProgressDialog):
+ '''
+ Use the QTimer singleShot method to dole out books one at
+ a time to the indicated callback function from action.py
+ '''
+ def __init__(self, gui, indices, callback_fn, db, db_type='calibre', status_msg_type='books', action_type=('Decrypting','Decryption')):
+ '''
+ :param gui: Parent gui
+ :param indices: List of Kobo books or list calibre book maps (indicated by param db_type)
+ :param callback_fn: the function from action.py that will do the heavy lifting (get_decrypted_kobo_books or add_new_books)
+ :param db: kobo database object or calibre database cache (indicated by param db_type)
+ :param db_type: string indicating what kind of database param db is
+ :param status_msg_type: string to indicate what the ProgressDialog is operating on (cosmetic only)
+ :param action_type: 2-Tuple of strings indicating what the ProgressDialog is doing to param status_msg_type (cosmetic only)
+ '''
+
+ self.total_count = len(indices)
+ QProgressDialog.__init__(self, '', 'Cancel', 0, self.total_count, gui)
+ self.setMinimumWidth(500)
+ self.indices, self.callback_fn, self.db, self.db_type = indices, callback_fn, db, db_type
+ self.action_type, self.status_msg_type = action_type, status_msg_type
+ self.gui = gui
+ self.setWindowTitle('{0} {1} {2}...'.format(self.action_type[0], self.total_count, self.status_msg_type))
+ self.i, self.successes, self.failures = 0, [], []
+ QTimer.singleShot(0, self.do_book_action)
+ self.exec_()
+
+ def do_book_action(self):
+ if self.wasCanceled():
+ return self.do_close()
+ if self.i >= self.total_count:
+ return self.do_close()
+ book = self.indices[self.i]
+ self.i += 1
+
+ # Get the title and build the caption and label text from the string parameters provided
+ if self.db_type == 'calibre':
+ dtitle = book[0].title
+ elif self.db_type == 'kobo':
+ dtitle = book.title
+ self.setWindowTitle('{0} {1} {2} ({3} {4} failures)...'.format(self.action_type[0], self.total_count,
+ self.status_msg_type, len(self.failures), self.action_type[1]))
+ self.setLabelText('{0}: {1}'.format(self.action_type[0], dtitle))
+ # If a calibre db, feed the calibre bookmap to action.py's add_new_books method
+ if self.db_type == 'calibre':
+ if self.callback_fn([book]):
+ self.successes.append(book)
+ else:
+ self.failures.append(book)
+ # If a kobo db, feed the index to the kobo book to action.py's get_decrypted_kobo_books method
+ elif self.db_type == 'kobo':
+ if self.callback_fn(book):
+ debug_print("DecryptAddProgressDialog::do_book_action - decrypted book: '%s'" % dtitle)
+ self.successes.append(book)
+ else:
+ debug_print("DecryptAddProgressDialog::do_book_action - book decryption failed: '%s'" % dtitle)
+ self.failures.append(book)
+ self.setValue(self.i)
+
+ # Lather, rinse, repeat.
+ QTimer.singleShot(0, self.do_book_action)
+
+ def do_close(self):
+ self.hide()
+ self.gui = None
+
+class AddEpubFormatsProgressDialog(QProgressDialog):
+ '''
+ Use the QTimer singleShot method to dole out epub formats one at
+ a time to the indicated callback function from action.py
+ '''
+ def __init__(self, gui, entries, callback_fn, status_msg_type='formats', action_type=('Adding','Added')):
+ '''
+ :param gui: Parent gui
+ :param entries: List of 3-tuples [(target calibre id, calibre metadata object, path to epub file)]
+ :param callback_fn: the function from action.py that will do the heavy lifting (process_epub_formats)
+ :param status_msg_type: string to indicate what the ProgressDialog is operating on (cosmetic only)
+ :param action_type: 2-tuple of strings indicating what the ProgressDialog is doing to param status_msg_type (cosmetic only)
+ '''
+
+ self.total_count = len(entries)
+ QProgressDialog.__init__(self, '', 'Cancel', 0, self.total_count, gui)
+ self.setMinimumWidth(500)
+ self.entries, self.callback_fn = entries, callback_fn
+ self.action_type, self.status_msg_type = action_type, status_msg_type
+ self.gui = gui
+ self.setWindowTitle('{0} {1} {2}...'.format(self.action_type[0], self.total_count, self.status_msg_type))
+ self.i, self.successes, self.failures = 0, [], []
+ QTimer.singleShot(0, self.do_book_action)
+ self.exec_()
+
+ def do_book_action(self):
+ if self.wasCanceled():
+ return self.do_close()
+ if self.i >= self.total_count:
+ return self.do_close()
+ epub_format = self.entries[self.i]
+ self.i += 1
+
+ # assign the elements of the 3-tuple details to legible variables
+ book_id, mi, path = epub_format[0], epub_format[1], epub_format[2]
+
+ # Get the title and build the caption and label text from the string parameters provided
+ dtitle = mi.title
+ self.setWindowTitle('{0} {1} {2} ({3} {4} failures)...'.format(self.action_type[0], self.total_count,
+ self.status_msg_type, len(self.failures), self.action_type[1]))
+ self.setLabelText('{0}: {1}'.format(self.action_type[0], dtitle))
+ # Send the necessary elements to the process_epub_formats callback function (action.py)
+ # and record the results
+ if self.callback_fn(book_id, mi, path):
+ self.successes.append((book_id, mi, path))
+ else:
+ self.failures.append((book_id, mi, path))
+ self.setValue(self.i)
+
+ # Lather, rinse, repeat
+ QTimer.singleShot(0, self.do_book_action)
+
+ def do_close(self):
+ self.hide()
+ self.gui = None
+
+class ViewLog(QDialog):
+ '''
+ Show a detailed summary of results as html.
+ '''
+ def __init__(self, title, html, parent=None):
+ '''
+ :param title: Caption for window title
+ :param html: HTML string log/report
+ '''
+ QDialog.__init__(self, parent)
+ self.l = l = QVBoxLayout()
+ self.setLayout(l)
+
+ self.tb = QTextBrowser(self)
+ QApplication.setOverrideCursor(Qt.WaitCursor)
+ # Rather than formatting the text in <pre> blocks like the calibre
+ # ViewLog does, instead just format it inside divs to keep style formatting
+ html = html.replace('\t','&nbsp;&nbsp;&nbsp;&nbsp;')#.replace('\n', '<br/>')
+ html = html.replace('> ','>&nbsp;')
+ self.tb.setHtml('<div>{0}</div>'.format(html))
+ QApplication.restoreOverrideCursor()
+ l.addWidget(self.tb)
+
+ self.bb = QDialogButtonBox(QDialogButtonBox.Ok)
+ self.bb.accepted.connect(self.accept)
+ self.bb.rejected.connect(self.reject)
+ self.copy_button = self.bb.addButton(_('Copy to clipboard'),
+ self.bb.ActionRole)
+ self.copy_button.setIcon(QIcon(I('edit-copy.png')))
+ self.copy_button.clicked.connect(self.copy_to_clipboard)
+ l.addWidget(self.bb)
+ self.setModal(False)
+ self.resize(QSize(700, 500))
+ self.setWindowTitle(title)
+ self.setWindowIcon(QIcon(I('dialog_information.png')))
+ self.show()
+
+ def copy_to_clipboard(self):
+ txt = self.tb.toPlainText()
+ QApplication.clipboard().setText(txt)
+
+
+class ResultsSummaryDialog(MessageBox):
+ def __init__(self, parent, title, msg, log='', det_msg=''):
+ '''
+ :param log: An HTML log
+ :param title: The title for this popup
+ :param msg: The msg to display
+ :param det_msg: Detailed message
+ '''
+ MessageBox.__init__(self, MessageBox.INFO, title, msg,
+ det_msg=det_msg, show_copy_button=False,
+ parent=parent)
+ self.log = log
+ self.vlb = self.bb.addButton(_('View Report'), self.bb.ActionRole)
+ self.vlb.setIcon(QIcon(I('dialog_information.png')))
+ self.vlb.clicked.connect(self.show_log)
+ self.det_msg_toggle.setVisible(bool(det_msg))
+ self.vlb.setVisible(bool(log))
+
+ def show_log(self):
+ self.log_viewer = ViewLog(PLUGIN_NAME + ' v' + PLUGIN_VERSION, self.log,
+ parent=self)
+
+
+class ReadOnlyTableWidgetItem(QTableWidgetItem):
+ def __init__(self, text):
+ if text is None:
+ text = ''
+ QTableWidgetItem.__init__(self, text, QTableWidgetItem.UserType)
+ self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
+
+class AuthorTableWidgetItem(ReadOnlyTableWidgetItem):
+ def __init__(self, text, sort_key):
+ ReadOnlyTableWidgetItem.__init__(self, text)
+ self.sort_key = sort_key
+
+ #Qt uses a simple < check for sorting items, override this to use the sortKey
+ def __lt__(self, other):
+ return self.sort_key < other.sort_key
+
+class SeriesTableWidgetItem(ReadOnlyTableWidgetItem):
+ def __init__(self, series, series_index=None):
+ display = ''
+ if series:
+ if series_index:
+ from calibre.ebooks.metadata import fmt_sidx
+ display = '%s [%s]' % (series, fmt_sidx(series_index))
+ self.sortKey = '%s%04d' % (series, series_index)
+ else:
+ display = series
+ self.sortKey = series
+ ReadOnlyTableWidgetItem.__init__(self, display)
+
+class IconWidgetItem(ReadOnlyTableWidgetItem):
+ def __init__(self, text, icon, sort_key):
+ ReadOnlyTableWidgetItem.__init__(self, text)
+ if icon:
+ self.setIcon(icon)
+ self.sort_key = sort_key
+
+ #Qt uses a simple < check for sorting items, override this to use the sortKey
+ def __lt__(self, other):
+ return self.sort_key < other.sort_key
+
+class NumericTableWidgetItem(QTableWidgetItem):
+
+ def __init__(self, number, is_read_only=False):
+ QTableWidgetItem.__init__(self, '', QTableWidgetItem.UserType)
+ self.setData(Qt.DisplayRole, number)
+ if is_read_only:
+ self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
+
diff --git a/Obok_calibre_plugin/obok_plugin/images/obok.png b/Obok_calibre_plugin/obok_plugin/images/obok.png
new file mode 100644
index 0000000..7217f0d
--- /dev/null
+++ b/Obok_calibre_plugin/obok_plugin/images/obok.png
Binary files differ
diff --git a/Obok_calibre_plugin/obok_plugin/obok/__init__.py b/Obok_calibre_plugin/obok_plugin/obok/__init__.py
new file mode 100644
index 0000000..7da8f17
--- /dev/null
+++ b/Obok_calibre_plugin/obok_plugin/obok/__init__.py
@@ -0,0 +1,4 @@
+# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
+
+__license__ = 'GPL v3'
+__docformat__ = 'restructuredtext en'
diff --git a/Obok_calibre_plugin/obok_plugin/obok/legacy_obok.py b/Obok_calibre_plugin/obok_plugin/obok/legacy_obok.py
new file mode 100644
index 0000000..f105688
--- /dev/null
+++ b/Obok_calibre_plugin/obok_plugin/obok/legacy_obok.py
@@ -0,0 +1,71 @@
+# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
+
+__license__ = 'GPL v3'
+__docformat__ = 'restructuredtext en'
+
+import os, sys
+import binascii, hashlib, re, string
+
+class legacy_obok(object):
+ def __init__(self):
+ self._userkey = ''
+
+ @property
+ def get_legacy_cookie_id(self):
+ if self._userkey != '':
+ return self._userkey
+ self._userkey = self.__oldcookiedeviceid()
+ return self._userkey
+
+ def __bytearraytostring(self, bytearr):
+ wincheck = re.match('@ByteArray\\((.+)\\)', bytearr)
+ if wincheck:
+ return wincheck.group(1)
+ return bytearr
+
+ def plist_to_dictionary(self, filename):
+ from subprocess import Popen, PIPE
+ from plistlib import readPlistFromString
+ 'Pipe the binary plist through plutil and parse the xml output'
+ with open(filename, 'rb') as f:
+ content = f.read()
+ args = ['plutil', '-convert', 'xml1', '-o', '-', '--', '-']
+ p = Popen(args, stdin=PIPE, stdout=PIPE)
+ p.stdin.write(content)
+ out, err = p.communicate()
+ return readPlistFromString(out)
+
+ def __oldcookiedeviceid(self):
+ '''Optionally attempt to get a device id using the old cookie method.
+ Must have _winreg installed on Windows machines for successful key retrieval.'''
+ wsuid = ''
+ pwsdid = ''
+ try:
+ if sys.platform.startswith('win'):
+ import _winreg
+ regkey_browser = _winreg.OpenKey(_winreg.HKEY_CURRENT_USER, 'Software\\Kobo\\Kobo Desktop Edition\\Browser')
+ cookies = _winreg.QueryValueEx(regkey_browser, 'cookies')
+ bytearrays = cookies[0]
+ elif sys.platform.startswith('darwin'):
+ prefs = os.path.join(os.environ['HOME'], 'Library/Preferences/com.kobo.Kobo Desktop Edition.plist')
+ cookies = self.plist_to_dictionary(prefs)
+ bytearrays = cookies['Browser.cookies']
+ for bytearr in bytearrays:
+ cookie = self.__bytearraytostring(bytearr)
+ wsuidcheck = re.match("^wsuid=([0-9a-f-]+)", cookie)
+ if(wsuidcheck):
+ wsuid = wsuidcheck.group(1)
+ pwsdidcheck = re.match('^pwsdid=([0-9a-f-]+)', cookie)
+ if (pwsdidcheck):
+ pwsdid = pwsdidcheck.group(1)
+ if (wsuid == '' or pwsdid == ''):
+ return None
+ preuserkey = string.join((pwsdid, wsuid), '')
+ userkey = hashlib.sha256(preuserkey).hexdigest()
+ return binascii.a2b_hex(userkey[32:])
+ except KeyError:
+ print ('No "cookies" key found in Kobo plist: no legacy user key found.')
+ return None
+ except:
+ print ('Error parsing Kobo plist: no legacy user key found.')
+ return None
diff --git a/Obok_calibre_plugin/obok_plugin/obok/obok.py b/Obok_calibre_plugin/obok_plugin/obok/obok.py
new file mode 100644
index 0000000..7288080
--- /dev/null
+++ b/Obok_calibre_plugin/obok_plugin/obok/obok.py
@@ -0,0 +1,482 @@
+#!/usr/bin/env python
+
+# Version 3.05 October 2014
+# Identifies DRM-free books in the dialog
+#
+# Version 3.04 September 2014
+# Handles DRM-free books as well (sometimes Kobo Library doesn't
+# show download link for DRM-free books)
+#
+# Version 3.03 August 2014
+# If PyCrypto is unavailable try to use libcrypto for AES_ECB.
+#
+# Version 3.02 August 2014
+# Relax checking of application/xhtml+xml and image/jpeg content.
+#
+# Version 3.01 June 2014
+# Check image/jpeg as well as application/xhtml+xml content. Fix typo
+# in Windows ipconfig parsing.
+#
+# Version 3.0 June 2014
+# Made portable for Mac and Windows, and the only module dependency
+# not part of python core is PyCrypto. Major code cleanup/rewrite.
+# No longer tries the first MAC address; tries them all if it detects
+# the decryption failed.
+#
+# Updated September 2013 by Anon
+# Version 2.02
+# Incorporated minor fixes posted at Apprentice Alf's.
+#
+# Updates July 2012 by Michael Newton
+# PWSD ID is no longer a MAC address, but should always
+# be stored in the registry. Script now works with OS X
+# and checks plist for values instead of registry. Must
+# have biplist installed for OS X support.
+#
+# Original comments left below; note the "AUTOPSY" is inaccurate. See
+# KoboLibrary.userkeys and KoboFile.decrypt()
+#
+##########################################################
+# KOBO DRM CRACK BY #
+# PHYSISTICATED #
+##########################################################
+# This app was made for Python 2.7 on Windows 32-bit
+#
+# This app needs pycrypto - get from here:
+# http://www.voidspace.org.uk/python/modules.shtml
+#
+# Usage: obok.py
+# Choose the book you want to decrypt
+#
+# Shouts to my krew - you know who you are - and one in
+# particular who gave me a lot of help with this - thank
+# you so much!
+#
+# Kopimi /K\
+# Keep sharing, keep copying, but remember that nothing is
+# for free - make sure you compensate your favorite
+# authors - and cut out the middle man whenever possible
+# ;) ;) ;)
+#
+# DRM AUTOPSY
+# The Kobo DRM was incredibly easy to crack, but it took
+# me months to get around to making this. Here's the
+# basics of how it works:
+# 1: Get MAC address of first NIC in ipconfig (sometimes
+# stored in registry as pwsdid)
+# 2: Get user ID (stored in tons of places, this gets it
+# from HKEY_CURRENT_USER\Software\Kobo\Kobo Desktop
+# Edition\Browser\cookies)
+# 3: Concatenate and SHA256, take the second half - this
+# is your master key
+# 4: Open %LOCALAPPDATA%\Kobo Desktop Editions\Kobo.sqlite
+# and dump content_keys
+# 5: Unbase64 the keys, then decode these with the master
+# key - these are your page keys
+# 6: Unzip EPUB of your choice, decrypt each page with its
+# page key, then zip back up again
+#
+# WHY USE THIS WHEN INEPT WORKS FINE? (adobe DRM stripper)
+# Inept works very well, but authors on Kobo can choose
+# what DRM they want to use - and some have chosen not to
+# let people download them with Adobe Digital Editions -
+# they would rather lock you into a single platform.
+#
+# With Obok, you can sync Kobo Desktop, decrypt all your
+# ebooks, and then use them on whatever device you want
+# - you bought them, you own them, you can do what you
+# like with them.
+#
+# Obok is Kobo backwards, but it is also means "next to"
+# in Polish.
+# When you buy a real book, it is right next to you. You
+# can read it at home, at work, on a train, you can lend
+# it to a friend, you can scribble on it, and add your own
+# explanations/translations.
+#
+# Obok gives you this power over your ebooks - no longer
+# are you restricted to one device. This allows you to
+# embed foreign fonts into your books, as older Kobo's
+# can't display them properly. You can read your books
+# on your phones, in different PC readers, and different
+# ereader devices. You can share them with your friends
+# too, if you like - you can do that with a real book
+# after all.
+#
+"""Manage all Kobo books, either encrypted or DRM-free."""
+import sys
+import os
+import subprocess
+import sqlite3
+import base64
+import binascii
+import re
+import zipfile
+import hashlib
+import xml.etree.ElementTree as ET
+import string
+import shutil
+
+class ENCRYPTIONError(Exception):
+ pass
+
+def _load_crypto_libcrypto():
+ from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \
+ Structure, c_ulong, create_string_buffer, cast
+ from ctypes.util import find_library
+
+ if sys.platform.startswith('win'):
+ libcrypto = find_library('libeay32')
+ else:
+ libcrypto = find_library('crypto')
+
+ if libcrypto is None:
+ raise ENCRYPTIONError('libcrypto not found')
+ libcrypto = CDLL(libcrypto)
+
+ AES_MAXNR = 14
+
+ c_char_pp = POINTER(c_char_p)
+ c_int_p = POINTER(c_int)
+
+ class AES_KEY(Structure):
+ _fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))),
+ ('rounds', c_int)]
+ AES_KEY_p = POINTER(AES_KEY)
+
+ def F(restype, name, argtypes):
+ func = getattr(libcrypto, name)
+ func.restype = restype
+ func.argtypes = argtypes
+ return func
+
+ AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',
+ [c_char_p, c_int, AES_KEY_p])
+ AES_ecb_encrypt = F(None, 'AES_ecb_encrypt',
+ [c_char_p, c_char_p, AES_KEY_p, c_int])
+
+ class AES(object):
+ def __init__(self, userkey):
+ self._blocksize = len(userkey)
+ if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
+ raise ENCRYPTIONError(_('AES improper key used'))
+ return
+ key = self._key = AES_KEY()
+ rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key)
+ if rv < 0:
+ raise ENCRYPTIONError(_('Failed to initialize AES key'))
+
+ def decrypt(self, data):
+ clear = ''
+ for i in range(0, len(data), 16):
+ out = create_string_buffer(16)
+ rv = AES_ecb_encrypt(data[i:i+16], out, self._key, 0)
+ if rv == 0:
+ raise ENCRYPTIONError(_('AES decryption failed'))
+ clear += out.raw
+ return clear
+
+ return AES
+
+def _load_crypto_pycrypto():
+ from Crypto.Cipher import AES as _AES
+ class AES(object):
+ def __init__(self, key):
+ self._aes = _AES.new(key, _AES.MODE_ECB)
+
+ def decrypt(self, data):
+ return self._aes.decrypt(data)
+
+ return AES
+
+def _load_crypto():
+ AES = None
+ cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto)
+ for loader in cryptolist:
+ try:
+ AES = loader()
+ break
+ except (ImportError, ENCRYPTIONError):
+ pass
+ return AES
+
+AES = _load_crypto()
+
+class KoboLibrary(object):
+ """The Kobo library.
+
+ This class represents all the information available from the data
+ written by the Kobo Desktop Edition application, including the list
+ of books, their titles, and the user's encryption key(s)."""
+
+ def __init__ (self):
+ if sys.platform.startswith('win'):
+ if sys.getwindowsversion().major > 5:
+ self.kobodir = os.environ['LOCALAPPDATA']
+ else:
+ self.kobodir = os.path.join(os.environ['USERPROFILE'], 'Local Settings', 'Application Data')
+ self.kobodir = os.path.join(self.kobodir, 'Kobo', 'Kobo Desktop Edition')
+ elif sys.platform.startswith('darwin'):
+ self.kobodir = os.path.join(os.environ['HOME'], 'Library', 'Application Support', 'Kobo', 'Kobo Desktop Edition')
+ self.bookdir = os.path.join(self.kobodir, 'kepub')
+ kobodb = os.path.join(self.kobodir, 'Kobo.sqlite')
+ self.__sqlite = sqlite3.connect(kobodb)
+ self.__cursor = self.__sqlite.cursor()
+ self._userkeys = []
+ self._books = []
+ self._volumeID = []
+
+ def close (self):
+ """Closes the database used by the library."""
+ self.__cursor.close()
+ self.__sqlite.close()
+
+ @property
+ def userkeys (self):
+ """The list of potential userkeys being used by this library.
+ Only one of these will be valid.
+ """
+ if len(self._userkeys) != 0:
+ return self._userkeys
+ userid = self.__getuserid()
+ for macaddr in self.__getmacaddrs():
+ self._userkeys.append(self.__getuserkey(macaddr, userid))
+ return self._userkeys
+
+ @property
+ def books (self):
+ """The list of KoboBook objects in the library."""
+ if len(self._books) != 0:
+ return self._books
+ """Drm-ed kepub"""
+ for row in self.__cursor.execute('SELECT DISTINCT volumeid, Title, Attribution, Series FROM content_keys, content WHERE contentid = volumeid'):
+ self._books.append(KoboBook(row[0], row[1], self.__bookfile(row[0]), 'kepub', self.__cursor, author=row[2], series=row[3]))
+ self._volumeID.append(row[0])
+ """Drm-free"""
+ for f in os.listdir(self.bookdir):
+ if(f not in self._volumeID):
+ row = self.__cursor.execute("SELECT Title, Attribution, Series FROM content WHERE ContentID = '" + f + "'").fetchone()
+ if row is not None:
+ fTitle = row[0]
+ self._books.append(KoboBook(f, fTitle, self.__bookfile(f), 'drm-free', self.__cursor, author=row[1], series=row[2]))
+ self._volumeID.append(f)
+ """Sort"""
+ self._books.sort(key=lambda x: x.title)
+ return self._books
+
+ def __bookfile (self, volumeid):
+ """The filename needed to open a given book."""
+ return os.path.join(self.kobodir, 'kepub', volumeid)
+
+ def __getmacaddrs (self):
+ """The list of all MAC addresses on this machine."""
+ macaddrs = []
+ if sys.platform.startswith('win'):
+ c = re.compile('\s(' + '[0-9a-f]{2}-' * 5 + '[0-9a-f]{2})(\s|$)', re.IGNORECASE)
+ for line in os.popen('ipconfig /all'):
+ m = c.search(line)
+ if m:
+ macaddrs.append(re.sub("-", ":", m.group(1)).upper())
+ elif sys.platform.startswith('darwin'):
+ c = re.compile('\s(' + '[0-9a-f]{2}:' * 5 + '[0-9a-f]{2})(\s|$)', re.IGNORECASE)
+ output = subprocess.check_output('/sbin/ifconfig -a', shell=True)
+ matches = c.findall(output)
+ for m in matches:
+ # print "m:",m[0]
+ macaddrs.append(m[0].upper())
+ return macaddrs
+
+ def __getuserid (self):
+ return self.__cursor.execute('SELECT UserID FROM user WHERE HasMadePurchase = "true"').fetchone()[0]
+
+ def __getuserkey (self, macaddr, userid):
+ deviceid = hashlib.sha256('NoCanLook' + macaddr).hexdigest()
+ userkey = hashlib.sha256(deviceid + userid).hexdigest()
+ return binascii.a2b_hex(userkey[32:])
+
+class KoboBook(object):
+ """A Kobo book.
+
+ A Kobo book contains a number of unencrypted and encrypted files.
+ This class provides a list of the encrypted files.
+
+ Each book has the following instance variables:
+ volumeid - a UUID which uniquely refers to the book in this library.
+ title - the human-readable book title.
+ filename - the complete path and filename of the book.
+ type - either kepub or drm-free"""
+ def __init__ (self, volumeid, title, filename, type, cursor, author=None, series=None):
+ self.volumeid = volumeid
+ self.title = title
+ self.author = author
+ self.series = series
+ self.series_index = None
+ self.filename = filename
+ self.type = type
+ self.__cursor = cursor
+ self._encryptedfiles = {}
+
+ @property
+ def encryptedfiles (self):
+ """A dictionary of KoboFiles inside the book.
+
+ The dictionary keys are the relative pathnames, which are
+ the same as the pathnames inside the book 'zip' file."""
+ if (self.type == 'drm-free'):
+ return self._encryptedfiles
+ if len(self._encryptedfiles) != 0:
+ return self._encryptedfiles
+ # Read the list of encrypted files from the DB
+ for row in self.__cursor.execute('SELECT elementid,elementkey FROM content_keys,content WHERE volumeid = ? AND volumeid = contentid',(self.volumeid,)):
+ self._encryptedfiles[row[0]] = KoboFile(row[0], None, base64.b64decode(row[1]))
+
+ # Read the list of files from the kepub OPF manifest so that
+ # we can get their proper MIME type.
+ # NOTE: this requires that the OPF file is unencrypted!
+ zin = zipfile.ZipFile(self.filename, "r")
+ xmlns = {
+ 'ocf': 'urn:oasis:names:tc:opendocument:xmlns:container',
+ 'opf': 'http://www.idpf.org/2007/opf'
+ }
+ ocf = ET.fromstring(zin.read('META-INF/container.xml'))
+ opffile = ocf.find('.//ocf:rootfile', xmlns).attrib['full-path']
+ basedir = re.sub('[^/]+$', '', opffile)
+ opf = ET.fromstring(zin.read(opffile))
+ zin.close()
+
+ c = re.compile('/')
+ for item in opf.findall('.//opf:item', xmlns):
+ mimetype = item.attrib['media-type']
+
+ # Convert relative URIs
+ href = item.attrib['href']
+ if not c.match(href):
+ href = string.join((basedir, href), '')
+
+ # Update books we've found from the DB.
+ if href in self._encryptedfiles:
+ self._encryptedfiles[href].mimetype = mimetype
+ return self._encryptedfiles
+
+ @property
+ def has_drm (self):
+ return not self.type == 'drm-free'
+
+
+class KoboFile(object):
+ """An encrypted file in a KoboBook.
+
+ Each file has the following instance variables:
+ filename - the relative pathname inside the book zip file.
+ mimetype - the file's MIME type, e.g. 'image/jpeg'
+ key - the encrypted page key."""
+
+ def __init__ (self, filename, mimetype, key):
+ self.filename = filename
+ self.mimetype = mimetype
+ self.key = key
+ def decrypt (self, userkey, contents):
+ """
+ Decrypt the contents using the provided user key and the
+ file page key. The caller must determine if the decrypted
+ data is correct."""
+ # The userkey decrypts the page key (self.key)
+ keyenc = AES(userkey)
+ decryptedkey = keyenc.decrypt(self.key)
+ # The decrypted page key decrypts the content
+ pageenc = AES(decryptedkey)
+ return self.__removeaespadding(pageenc.decrypt(contents))
+
+ def check (self, contents):
+ """
+ If the contents uses some known MIME types, check if it
+ conforms to the type. Throw a ValueError exception if not.
+ If the contents uses an uncheckable MIME type, don't check
+ it and don't throw an exception.
+ Returns True if the content was checked, False if it was not
+ checked."""
+ if self.mimetype == 'application/xhtml+xml':
+ if contents[:5]=="<?xml":
+ return True
+ else:
+ print "Bad XML: ",contents[:5]
+ raise ValueError
+ if self.mimetype == 'image/jpeg':
+ if contents[:3] == '\xff\xd8\xff':
+ return True
+ else:
+ print "Bad JPEG: ", contents[:3].encode('hex')
+ raise ValueError()
+ return False
+
+ def __removeaespadding (self, contents):
+ """
+ Remove the trailing padding, using what appears to be the CMS
+ algorithm from RFC 5652 6.3"""
+ lastchar = binascii.b2a_hex(contents[-1:])
+ strlen = int(lastchar, 16)
+ padding = strlen
+ if strlen == 1:
+ return contents[:-1]
+ if strlen < 16:
+ for i in range(strlen):
+ testchar = binascii.b2a_hex(contents[-strlen:-(strlen-1)])
+ if testchar != lastchar:
+ padding = 0
+ if padding > 0:
+ contents = contents[:-padding]
+ return contents
+
+if __name__ == '__main__':
+
+ lib = KoboLibrary()
+
+ for i, book in enumerate(lib.books):
+ print ('%d: %s' % (i + 1, book.title)).encode('ascii', 'ignore')
+
+ num_string = raw_input("Convert book number... ")
+ try:
+ num = int(num_string)
+ book = lib.books[num - 1]
+ except (ValueError, IndexError):
+ exit()
+
+ print "Converting", book.title
+
+ zin = zipfile.ZipFile(book.filename, "r")
+ # make filename out of Unicode alphanumeric and whitespace equivalents from title
+ outname = "%s.epub" % (re.sub('[^\s\w]', '', book.title, 0, re.UNICODE))
+
+ if (book.type == 'drm-free'):
+ print "DRM-free book, conversion is not needed"
+ shutil.copyfile(book.filename, outname)
+ print "Book saved as", os.path.join(os.getcwd(), outname)
+ exit(0)
+
+ result = 1
+ for userkey in lib.userkeys:
+ # print "Trying key: ",userkey.encode('hex_codec')
+ confirmedGood = False
+ try:
+ zout = zipfile.ZipFile(outname, "w", zipfile.ZIP_DEFLATED)
+ for filename in zin.namelist():
+ contents = zin.read(filename)
+ if filename in book.encryptedfiles:
+ file = book.encryptedfiles[filename]
+ contents = file.decrypt(userkey, contents)
+ # Parse failures mean the key is probably wrong.
+ if not confirmedGood:
+ confirmedGood = file.check(contents)
+ zout.writestr(filename, contents)
+ zout.close()
+ print "Book saved as", os.path.join(os.getcwd(), outname)
+ result = 0
+ break
+ except ValueError:
+ print "Decryption failed, trying next key"
+ zout.close()
+ os.remove(outname)
+
+ zin.close()
+ lib.close()
+ exit(result)
diff --git a/Obok_calibre_plugin/obok_plugin/obok_dedrm_Help.htm b/Obok_calibre_plugin/obok_plugin/obok_dedrm_Help.htm
new file mode 100644
index 0000000..d3cc27c
--- /dev/null
+++ b/Obok_calibre_plugin/obok_plugin/obok_dedrm_Help.htm
@@ -0,0 +1,37 @@
+<html>
+
+<head>
+<meta http-equiv="content-type" content="text/html; charset=utf-8">
+<title>Obok DeDRM Plugin Configuration</title>
+</head>
+
+<body>
+
+<h1>Obok DeDRM Plugin</h1>
+<h3>(version 3.1.0)</h3>
+<h3> Headlines and links.</h3>
+
+<p>Stuff.</p>
+
+<p>And more stuff.</p>
+
+<h3>Installation:</h3>
+
+<p>The ususal method of Preferences -> Plugins -> Load plugin from file.</p>
+
+
+<h3>Configuration:</h3>
+
+<p>There is no configuration (other than to choose what menus to add obok to)</p>
+
+
+<h3>Troubleshooting:</h3>
+
+<p >If you find that it’s not working for you , you can save a lot of time by using the plugin with Calibre in debug mode. This will print out a lot of helpful info that can be copied into any online help requests.</p>
+
+<p>Open a command prompt (terminal window) and type "calibre-debug -g" (without the quotes). Calibre will launch, and you can use the plugin the usual way. The debug info will be output to the original command prompt (terminal window). Copy the resulting output and paste it into the comment you make at my blog.</p>
+<p><span class="bold">Note:</span> The Mac version of Calibre doesn’t install the command line tools by default. If you go to the ‘Preferences’ page and click on the miscellaneous button, you’ll find the option to install the command line tools.</p>
+
+</body>
+
+</html>
diff --git a/Obok_calibre_plugin/obok_plugin/plugin-import-name-obok_dedrm.txt b/Obok_calibre_plugin/obok_plugin/plugin-import-name-obok_dedrm.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/Obok_calibre_plugin/obok_plugin/plugin-import-name-obok_dedrm.txt
diff --git a/Obok_calibre_plugin/obok_plugin/translations/de.po b/Obok_calibre_plugin/obok_plugin/translations/de.po
new file mode 100644
index 0000000..05b6d4b
--- /dev/null
+++ b/Obok_calibre_plugin/obok_plugin/translations/de.po
@@ -0,0 +1,102 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: obok\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-10-19 10:28+0200\n"
+"PO-Revision-Date: 2014-10-23 14:43+0100\n"
+"Last-Translator: \n"
+"Language-Team: friends of obok\n"
+"Language: de\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Generator: Poedit 1.6.10\n"
+
+#: common_utils.py:220
+msgid "Help"
+msgstr "Hilfe"
+
+#: common_utils.py:229 utilities.py:207
+msgid "Restart required"
+msgstr "Neustart erforderlich"
+
+#: common_utils.py:230 utilities.py:208
+msgid ""
+"Title image not found - you must restart Calibre before using this plugin!"
+msgstr ""
+"Das Abbild wurde nicht gefunden. - vor der Verwendung dieses Calibre Plugin "
+"is ein Neustart erforderlich!"
+
+#: common_utils.py:316
+msgid "Undefined"
+msgstr "Undefiniert"
+
+#: config.py:25
+msgid ""
+"<p>Default behavior when duplicates are detected. None of the choices will "
+"cause calibre ebooks to be overwritten"
+msgstr ""
+"<p>Standardverhalten, wenn Duplikate erkannt werden. Keine der "
+"Entscheidungen werden ebooks verursachen das sie überschrieben werden."
+
+#: dialogs.py:58
+msgid "Obok DeDRM"
+msgstr "Obok DeDRM"
+
+#: dialogs.py:68
+msgid "<a href=\"http://www.foo.com/\">Help</a>"
+msgstr "<a href=\"http://www.foo.com/\">Hilfe</a>"
+
+#: dialogs.py:82
+msgid "Select All"
+msgstr "Alles markieren"
+
+#: dialogs.py:83
+msgid "Select all books to add them to the calibre library."
+msgstr "Wählen Sie alle Bücher, um sie zu Calibre Bibliothek hinzuzufügen."
+
+#: dialogs.py:85
+msgid "All with DRM"
+msgstr "Alle mit DRM"
+
+#: dialogs.py:86
+msgid "Select all books with DRM."
+msgstr "Wählen Sie alle Bücher mit DRM."
+
+#: dialogs.py:88
+msgid "All DRM free"
+msgstr "Alle ohne DRM"
+
+#: dialogs.py:89
+msgid "Select all books without DRM."
+msgstr "Wählen Sie alle Bücher ohne DRM."
+
+#: dialogs.py:139
+msgid "Title"
+msgstr "Titel"
+
+#: dialogs.py:139
+msgid "Author"
+msgstr "Autor"
+
+#: dialogs.py:139
+msgid "Series"
+msgstr "Reihe"
+
+#: dialogs.py:362
+msgid "Copy to clipboard"
+msgstr "In Zwischenablage kopieren"
+
+#: dialogs.py:390
+msgid "View Report"
+msgstr "Bericht anzeigen"
+
+#: __init__.py:24
+msgid "Removes DRM from Kobo kepubs and adds them to the library."
+msgstr "Entfernt DRM von Kobo kepubs und fügt sie zu Bibliothek hinzu."
diff --git a/Obok_calibre_plugin/obok_plugin/translations/default.po b/Obok_calibre_plugin/obok_plugin/translations/default.po
new file mode 100644
index 0000000..2b73c84
--- /dev/null
+++ b/Obok_calibre_plugin/obok_plugin/translations/default.po
@@ -0,0 +1,335 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-17 12:51+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <[email protected]>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=CHARSET\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:80
+msgid ""
+"<p>No books found in Kobo Library\n"
+"Are you sure it's installed\\configured\\synchronized?"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:87
+msgid "Legacy key found: "
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:93
+msgid "Trouble retrieving keys with newer obok method."
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:97
+msgid "Found {0} possible keys to try."
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:99
+msgid "<p>No userkeys found to decrypt books with. No point in proceeding."
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:115
+msgid "{} - Decryption canceled by user."
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:135
+msgid "{} - \"Add books\" canceled by user."
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:137
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:156
+msgid "{} - wrapping up results."
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:153
+msgid "{} - User opted not to try to insert EPUB formats"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:188
+msgid "{0} - Decrypting {1}"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:197
+msgid "{0} - Couldn't decrypt {1}"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:198
+msgid "decryption errors"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:213
+msgid "{0} - Added {1}"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:218
+msgid "{0} - {1} already exists. Will try to add format later."
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:219
+msgid "duplicate detected"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:233
+msgid "{0} - Successfully added EPUB format to existing {1}"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:236
+msgid ""
+"{0} - Error adding EPUB format to existing {1}. This really shouldn't happen."
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:259
+msgid "{} - \"Insert formats\" canceled by user."
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:291
+msgid ""
+"<p><b>{0}</b> EPUB{2} successfully added to library.<br /><br /><b>{1}</b> "
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:292
+msgid ""
+"not added because books with the same title/author were detected.<br /><br /"
+">Would you like to try and add the EPUB format{0}"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:293
+msgid ""
+" to those existing entries?<br /><br />NOTE: no pre-existing EPUBs will be "
+"overwritten."
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:295
+msgid ""
+"{0} -- not added because of {1} in your library.\n"
+"\n"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:297
+msgid "<p><b>{0}</b> -- not added because of {1} in your library.<br /><br />"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:298
+msgid ""
+"Would you like to try and add the EPUB format to an available calibre "
+"duplicate?<br /><br />"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:299
+msgid "NOTE: no pre-existing EPUB will be overwritten."
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:346
+msgid "Trying key: "
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:378
+msgid "Decryption failed, trying next key."
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:382
+msgid "Unknown Error decrypting, trying next key.."
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:395
+msgid ""
+"<p>All selected Kobo books added as new calibre books or inserted into "
+"existing calibre ebooks.<br /><br />No issues."
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:399
+msgid "<p>{0} successfully added."
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:403
+msgid ""
+"<p>Not all selected Kobo books made it into calibre.<br /><br />View report "
+"for details."
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:404
+msgid "<p><b>Total attempted:</b> {}</p>\n"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:405
+msgid "<p><b>Decryption errors:</b> {}</p>\n"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:411
+msgid "<p><b>New Books created:</b> {}</p>\n"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:418
+msgid "<p><b>Duplicates that weren't added:</b> {}</p>\n"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:426
+msgid "<p><b>Book imports cancelled by user:</b> {}</p>\n"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:428
+msgid ""
+"<p><b>New EPUB formats inserted in existing calibre books:</b> {0}</p>\n"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:434
+msgid ""
+"<p><b>EPUB formats NOT inserted into existing calibre books:</b> {}<br />\n"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:435
+msgid ""
+"(Either because the user <i>chose</i> not to insert them, or because all "
+"duplicates already had an EPUB format)"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:444
+msgid "<p><b>Format imports cancelled by user:</b> {}</p>\n"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:458
+msgid "Unknown Book Title"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:460
+msgid "it couldn't be decrypted."
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:462
+msgid ""
+"user CHOSE not to insert the new EPUB format, or all existing calibre "
+"entries HAD an EPUB format already."
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:464
+msgid "of unknown reasons. Gosh I'm embarrassed!"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:465
+msgid "<p>{0} not added because {1}"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:226
+msgid "Help"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:235
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\utilities.py:214
+msgid "Restart required"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:236
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\utilities.py:215
+msgid ""
+"Title image not found - you must restart Calibre before using this plugin!"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:322
+msgid "Undefined"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:30
+msgid "When should Obok try to insert EPUBs into existing calibre entries?"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:33
+msgid ""
+"<p>Default behavior when duplicates are detected. None of the choices will "
+"cause calibre ebooks to be overwritten"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:35
+msgid "Ask"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:35
+msgid "Always"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:35
+msgid "Never"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:60
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\utilities.py:150
+msgid " v"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:65
+msgid "Obok DeDRM"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:68
+msgid "<a href=\"http://www.foo.com/\">Help</a>"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:89
+msgid "Select All"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:90
+msgid "Select all books to add them to the calibre library."
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:92
+msgid "All with DRM"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:93
+msgid "Select all books with DRM."
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:95
+msgid "All DRM free"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:96
+msgid "Select all books without DRM."
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:146
+msgid "Title"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:146
+msgid "Author"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:146
+msgid "Series"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:369
+msgid "Copy to clipboard"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:397
+msgid "View Report"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\__init__.py:21
+msgid "Removes DRM from Kobo kepubs and adds them to the library."
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:162
+msgid "AES improper key used"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:167
+msgid "Failed to initialize AES key"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:175
+msgid "AES decryption failed"
+msgstr ""
diff --git a/Obok_calibre_plugin/obok_plugin/translations/es.po b/Obok_calibre_plugin/obok_plugin/translations/es.po
new file mode 100644
index 0000000..6ca0718
--- /dev/null
+++ b/Obok_calibre_plugin/obok_plugin/translations/es.po
@@ -0,0 +1,419 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: obok\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-17 12:51+0100\n"
+"PO-Revision-Date: 2014-11-17 21:32+0100\n"
+"Last-Translator: Friends of obok\n"
+"Language-Team: friends of obok\n"
+"Language: es\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Generator: Poedit 1.6.10\n"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:80
+msgid ""
+"<p>No books found in Kobo Library\n"
+"Are you sure it's installed\\configured\\synchronized?"
+msgstr ""
+"<p>No se han encontrado libros en la biblioteca de Kobo\n"
+"¿Estás seguro que está instalada\\configurada\\sincronizada?"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:87
+msgid "Legacy key found: "
+msgstr "Clave antigua localizada:"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:93
+msgid "Trouble retrieving keys with newer obok method."
+msgstr "Problema al obtener las claves con el nuevo método obok"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:97
+msgid "Found {0} possible keys to try."
+msgstr "Localizadas {0} posibles claves que probar."
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:99
+msgid "<p>No userkeys found to decrypt books with. No point in proceeding."
+msgstr ""
+"<p>No se han encontrado claves de usuarios con las que desencriptar los "
+"libros. No tiene sentido proceder."
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:115
+msgid "{} - Decryption canceled by user."
+msgstr "{} - Desencriptación cancelada por el usuario"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:135
+msgid "{} - \"Add books\" canceled by user."
+msgstr "{} - \"Añadir libros\" cancelado por el usuario."
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:137
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:156
+msgid "{} - wrapping up results."
+msgstr "{} - Preparando resultados."
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:153
+msgid "{} - User opted not to try to insert EPUB formats"
+msgstr "{} - El usuario optó por no tratar de insertar los formatos EPUB."
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:188
+msgid "{0} - Decrypting {1}"
+msgstr "{0} - Desencriptando {1}"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:197
+msgid "{0} - Couldn't decrypt {1}"
+msgstr "{0} - No se pudo desencriptar {1}"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:198
+msgid "decryption errors"
+msgstr "errores de desencriptación"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:213
+msgid "{0} - Added {1}"
+msgstr "{0} - Añadido {1}"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:218
+msgid "{0} - {1} already exists. Will try to add format later."
+msgstr "{0} - {1} ya existe. Se tratará de añadir el formato más tarde."
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:219
+msgid "duplicate detected"
+msgstr "detectado un duplicado"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:233
+msgid "{0} - Successfully added EPUB format to existing {1}"
+msgstr "{0} - Formato EPUB añadido con éxito al {1} existente"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:236
+msgid ""
+"{0} - Error adding EPUB format to existing {1}. This really shouldn't happen."
+msgstr ""
+"{0} - Error al añadir el formato EPUB al existente {1}. Esto realmente no "
+"debería ocurrir."
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:259
+msgid "{} - \"Insert formats\" canceled by user."
+msgstr "{} - \"Insertar formatos\" cancelado por el usuario."
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:291
+msgid ""
+"<p><b>{0}</b> EPUB{2} successfully added to library.<br /><br /><b>{1}</b> "
+msgstr ""
+"<p><b>{0}</b> EPUB({2}) añadido({2}) con éxito a la biblioteca.<br /><br /"
+"><b>{1}</b> "
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:292
+msgid ""
+"not added because books with the same title/author were detected.<br /><br /"
+">Would you like to try and add the EPUB format{0}"
+msgstr ""
+"no añadido({0}) porque se han detectado libros con el mismo título/autor."
+"<br /><br />¿Deseas añadir el formato EPUB"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:293
+msgid ""
+" to those existing entries?<br /><br />NOTE: no pre-existing EPUBs will be "
+"overwritten."
+msgstr ""
+" a las entradas existentes?<br /><br />NOTA: no se sobreescribirá ningún "
+"EPUB que ya existiera."
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:295
+msgid ""
+"{0} -- not added because of {1} in your library.\n"
+"\n"
+msgstr ""
+"{0} -- no añadido porque {1} está en tu biblioteca.\n"
+"\n"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:297
+msgid "<p><b>{0}</b> -- not added because of {1} in your library.<br /><br />"
+msgstr ""
+"<p><b>{0}</b> -- no se ha añadido porque se ha {1}, que está en tu "
+"biblioteca.<br /><br />"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:298
+msgid ""
+"Would you like to try and add the EPUB format to an available calibre "
+"duplicate?<br /><br />"
+msgstr ""
+"¿Desearías añadir el formato EPUB al elemento que ya está disponible en "
+"calibre?<br /><br />"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:299
+msgid "NOTE: no pre-existing EPUB will be overwritten."
+msgstr "NOTA: no se sobreescribirá ningún EPUB que ya existiera."
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:346
+msgid "Trying key: "
+msgstr "Probando clave:"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:378
+msgid "Decryption failed, trying next key."
+msgstr "La desencriptación falló, probando la clave siguiente."
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:382
+msgid "Unknown Error decrypting, trying next key.."
+msgstr "Error desconocido al desencriptar, probando siguiente clave..."
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:395
+msgid ""
+"<p>All selected Kobo books added as new calibre books or inserted into "
+"existing calibre ebooks.<br /><br />No issues."
+msgstr ""
+"<p>Todos los libros de Kobo seleccionados se han añadido a calibre como "
+"nuevos libros o en libros ya existentes.<br /><br />Sin problemas."
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:399
+msgid "<p>{0} successfully added."
+msgstr "<p><b>{0}</b> añadido con éxito."
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:403
+msgid ""
+"<p>Not all selected Kobo books made it into calibre.<br /><br />View report "
+"for details."
+msgstr ""
+"<p>No se han añadido a calibre todos los libros de Kobo seleccionados.<br /"
+"><br />Comprueba el informe para obtener los detalles."
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:404
+msgid "<p><b>Total attempted:</b> {}</p>\n"
+msgstr "<p><b>Intentados en total:</b> {}</p>\n"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:405
+msgid "<p><b>Decryption errors:</b> {}</p>\n"
+msgstr "<p><b>Errores de desencriptación:</b> {}</p>\n"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:411
+msgid "<p><b>New Books created:</b> {}</p>\n"
+msgstr "<p><b>Nuevos libros creados:</b> {}</p>\n"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:418
+msgid "<p><b>Duplicates that weren't added:</b> {}</p>\n"
+msgstr "<p><b>Duplicados que no se han añadido:</b> {}</p>\n"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:426
+msgid "<p><b>Book imports cancelled by user:</b> {}</p>\n"
+msgstr "<p><b>Importación de libros cancelada por el usuario:</b> {}</p>\n"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:428
+msgid ""
+"<p><b>New EPUB formats inserted in existing calibre books:</b> {0}</p>\n"
+msgstr ""
+"<p><b>Nuevos formatos EPUB insertados en libros existentes en calibre:</b> "
+"{0}</p>\n"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:434
+msgid ""
+"<p><b>EPUB formats NOT inserted into existing calibre books:</b> {}<br />\n"
+msgstr ""
+"<p><b>Formatos EPUB NO insertados en libros de calibre existentes:</b> {}"
+"<br />\n"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:435
+msgid ""
+"(Either because the user <i>chose</i> not to insert them, or because all "
+"duplicates already had an EPUB format)"
+msgstr ""
+"(Bien porque el usuario <i>eligió</i> no insertarlos, o porque todos los "
+"duplicados ya tenían un formato EPUB)"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:444
+msgid "<p><b>Format imports cancelled by user:</b> {}</p>\n"
+msgstr "<p><b>Importación de formatos cancelada por el usuario:</b> {}</p>\n"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:458
+msgid "Unknown Book Title"
+msgstr "Título de libro desconocido"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:460
+msgid "it couldn't be decrypted."
+msgstr "no se podía desencriptar."
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:462
+msgid ""
+"user CHOSE not to insert the new EPUB format, or all existing calibre "
+"entries HAD an EPUB format already."
+msgstr ""
+"el usuario ELIGIÓ no insertar el nuevo formato EPUB o todas las entradas de "
+"calibre existentes ya TENÍAN un formato EPUB."
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:464
+msgid "of unknown reasons. Gosh I'm embarrassed!"
+msgstr "por razones desconocidas. ¡Dios, qué vergüenza!"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:465
+msgid "<p>{0} not added because {1}"
+msgstr "<p><b>{0}</b> no se ha añadido porque {1}"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:226
+msgid "Help"
+msgstr "Ayuda"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:235
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\utilities.py:214
+msgid "Restart required"
+msgstr "Se necesita reiniciar"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:236
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\utilities.py:215
+msgid ""
+"Title image not found - you must restart Calibre before using this plugin!"
+msgstr ""
+"Imagen del título no encontrada - ¡debes reiniciar Calibre antes de usar "
+"este plugin!"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:322
+msgid "Undefined"
+msgstr "Indefinido"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:30
+msgid "When should Obok try to insert EPUBs into existing calibre entries?"
+msgstr ""
+"¿Cuándo debería Obok tratar de insertar EPUB en las entradas de calibre que "
+"ya existen?"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:33
+msgid ""
+"<p>Default behavior when duplicates are detected. None of the choices will "
+"cause calibre ebooks to be overwritten"
+msgstr ""
+"<p>Comportamiento por defecto cuando se detectan duplicados. Ninguna de las "
+"opciones provocará que se sobreescriban los libros en calibre."
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:35
+msgid "Ask"
+msgstr "Preguntar"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:35
+msgid "Always"
+msgstr "Siempre"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:35
+msgid "Never"
+msgstr "Nunca"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:60
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\utilities.py:150
+msgid " v"
+msgstr "v"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:65
+msgid "Obok DeDRM"
+msgstr "Obok DeDRM"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:68
+msgid "<a href=\"http://www.foo.com/\">Help</a>"
+msgstr "<a href=\"http://www.foo.com/\">Ayuda</a>"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:89
+msgid "Select All"
+msgstr "Seleccionar todo"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:90
+msgid "Select all books to add them to the calibre library."
+msgstr ""
+"Seleccionar todos los libros para añadirlos a la biblioteca de calibre."
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:92
+msgid "All with DRM"
+msgstr "Todos con DRM"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:93
+msgid "Select all books with DRM."
+msgstr "Seleccionar todos los libros con DRM."
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:95
+msgid "All DRM free"
+msgstr "Todos sin DRM"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:96
+msgid "Select all books without DRM."
+msgstr "Seleccionar todos los libros sin DRM."
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:146
+msgid "Title"
+msgstr "Título"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:146
+msgid "Author"
+msgstr "Autor"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:146
+msgid "Series"
+msgstr "Serie"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:369
+msgid "Copy to clipboard"
+msgstr "Copiar al portapapeles"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:397
+msgid "View Report"
+msgstr "Ver informe"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\__init__.py:21
+msgid "Removes DRM from Kobo kepubs and adds them to the library."
+msgstr "Elimina el DRM de kepubs de Kobo y los añade a la biblioteca."
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:162
+msgid "AES improper key used"
+msgstr "Utilizada clave AES inapropiada"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:167
+msgid "Failed to initialize AES key"
+msgstr "Fallo al inicializar clave AES"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:175
+msgid "AES decryption failed"
+msgstr "Fallo de desencriptación AES"
+
+#~ msgid ""
+#~ "<p>No Kobo Library found\n"
+#~ "Are you sure it's installed\\configured\\synchronized?"
+#~ msgstr ""
+#~ "<p>No se ha encontrado la biblioteca de Kobo\n"
+#~ "¿Estás seguro que está instalada\\configurada\\sincronizada?"
+
+#~ msgid "Decryption"
+#~ msgstr "Desencriptación"
+
+#~ msgid "Adding"
+#~ msgstr "Añadiendo"
+
+#~ msgid "Addition"
+#~ msgstr "Adición"
+
+#~ msgid "new calibre books"
+#~ msgstr "nuevos libros de calibre"
+
+#~ msgid " (*: drm - free)"
+#~ msgstr "(*: sin drm)"
+
+#~ msgid "* : drm - free"
+#~ msgstr "*: sin drm"
+
+#~ msgid "You must make a selection!"
+#~ msgstr "¡Debes seleccionar algo!"
+
+#~ msgid "Cancel"
+#~ msgstr "Cancelar"
+
+#~ msgid "{0} {1} {2} ({3} {4} failures)..."
+#~ msgstr "{0} {1} {2} ({3} {4} fallos)..."
+
+#~ msgid "Added"
+#~ msgstr "Añadido"
+
+#~ msgid "formats"
+#~ msgstr "formatos"
+
+#~ msgid "Yes"
+#~ msgstr "Sí"
+
+#~ msgid "No"
+#~ msgstr "No"
diff --git a/Obok_calibre_plugin/obok_plugin/translations/nl.po b/Obok_calibre_plugin/obok_plugin/translations/nl.po
new file mode 100644
index 0000000..24490c3
--- /dev/null
+++ b/Obok_calibre_plugin/obok_plugin/translations/nl.po
@@ -0,0 +1,102 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: obok\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-10-19 10:28+0200\n"
+"PO-Revision-Date: 2014-10-23 14:08+0100\n"
+"Last-Translator: \n"
+"Language-Team: friends of obok\n"
+"Language: nl\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Generator: Poedit 1.6.10\n"
+
+#: common_utils.py:220
+msgid "Help"
+msgstr "Help"
+
+#: common_utils.py:229 utilities.py:207
+msgid "Restart required"
+msgstr "Opnieuw opstarten vereist"
+
+#: common_utils.py:230 utilities.py:208
+msgid ""
+"Title image not found - you must restart Calibre before using this plugin!"
+msgstr ""
+"Afbeelding niet gevonden. - Calibre moet opnieuw opgestart worden voordat "
+"deze plugin kan worden gebruikt!"
+
+#: common_utils.py:316
+msgid "Undefined"
+msgstr "Niet gedefinieerd"
+
+#: config.py:25
+msgid ""
+"<p>Default behavior when duplicates are detected. None of the choices will "
+"cause calibre ebooks to be overwritten"
+msgstr ""
+"<p>Standaard gedrag wanneer er duplicaten worden geconstateerd. Geen van de "
+"opties zal reeds bestaande ebooks in de Calibre bibliotheek overschrijven."
+
+#: dialogs.py:58
+msgid "Obok DeDRM"
+msgstr "Obok DeDRM"
+
+#: dialogs.py:68
+msgid "<a href=\"http://www.foo.com/\">Help</a>"
+msgstr "<a href=\"http://www.foo.com/\">Help</a>"
+
+#: dialogs.py:82
+msgid "Select All"
+msgstr "Alles selecteren"
+
+#: dialogs.py:83
+msgid "Select all books to add them to the calibre library."
+msgstr "Alle boeken selecteren om ze aan de Calibre bibliotheek toe te voegen."
+
+#: dialogs.py:85
+msgid "All with DRM"
+msgstr "Alle met DRM"
+
+#: dialogs.py:86
+msgid "Select all books with DRM."
+msgstr "Alle boeken met DRM selecteren."
+
+#: dialogs.py:88
+msgid "All DRM free"
+msgstr "Alle zonder DRM"
+
+#: dialogs.py:89
+msgid "Select all books without DRM."
+msgstr "Alle boeken zonder DRM selecteren."
+
+#: dialogs.py:139
+msgid "Title"
+msgstr "Titel"
+
+#: dialogs.py:139
+msgid "Author"
+msgstr "Auteur"
+
+#: dialogs.py:139
+msgid "Series"
+msgstr "Reeks/serie"
+
+#: dialogs.py:362
+msgid "Copy to clipboard"
+msgstr "Naar het Klembord kopiëren"
+
+#: dialogs.py:390
+msgid "View Report"
+msgstr "Rapport weergeven"
+
+#: __init__.py:24
+msgid "Removes DRM from Kobo kepubs and adds them to the library."
+msgstr "Verwijdert de DRM van Kobo kepubs en voegt ze toe aan de bibliotheek."
diff --git a/Obok_calibre_plugin/obok_plugin/utilities.py b/Obok_calibre_plugin/obok_plugin/utilities.py
new file mode 100644
index 0000000..62e305c
--- /dev/null
+++ b/Obok_calibre_plugin/obok_plugin/utilities.py
@@ -0,0 +1,228 @@
+# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
+from __future__ import (unicode_literals, division, absolute_import,
+ print_function)
+
+__license__ = 'GPL v3'
+__docformat__ = 'restructuredtext en'
+
+
+import os, struct, time
+from StringIO import StringIO
+from traceback import print_exc
+
+try:
+ from PyQt5.Qt import (Qt, QDialog, QPixmap, QIcon, QLabel, QHBoxLayout, QFont, QTableWidgetItem)
+except ImportError:
+ from PyQt4.Qt import (Qt, QDialog, QPixmap, QIcon, QLabel, QHBoxLayout, QFont, QTableWidgetItem)
+
+from calibre.utils.config import config_dir
+from calibre.constants import iswindows, DEBUG
+from calibre import prints
+from calibre.gui2 import (error_dialog, gprefs)
+from calibre.gui2.actions import menu_action_unique_name
+
+from calibre_plugins.obok_dedrm.__init__ import (PLUGIN_NAME,
+ PLUGIN_SAFE_NAME, PLUGIN_VERSION, PLUGIN_DESCRIPTION)
+
+plugin_ID = None
+plugin_icon_resources = {}
+
+try:
+ from calibre.gui2 import QVariant
+ del QVariant
+except ImportError:
+ is_qt4 = False
+ convert_qvariant = lambda x: x
+else:
+ is_qt4 = True
+
+ def convert_qvariant(x):
+ vt = x.type()
+ if vt == x.String:
+ return unicode(x.toString())
+ if vt == x.List:
+ return [convert_qvariant(i) for i in x.toList()]
+ return x.toPyObject()
+
+BASE_TIME = None
+def debug_print(*args):
+ global BASE_TIME
+ if BASE_TIME is None:
+ BASE_TIME = time.time()
+ if DEBUG:
+ prints('DEBUG: %6.1f'%(time.time()-BASE_TIME), *args)
+
+try:
+ debug_print("obok::utilities.py - loading translations")
+ load_translations()
+except NameError:
+ debug_print("obok::utilities.py - exception when loading translations")
+ pass # load_translations() added in calibre 1.9
+
+def format_plural(number, possessive=False):
+ '''
+ Cosmetic ditty to provide the proper string formatting variable to handle singular/plural situations
+
+ :param: number: variable that represents the count/len of something
+ '''
+ if not possessive:
+ return '' if number == 1 else 's'
+ return '\'s' if number == 1 else 's\''
+
+
+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_ID
+ plugin_ID = 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/'
+ '''
+ 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_ID:
+ local_images_dir = get_local_images_dir(plugin_ID)
+ 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 showErrorDlg(errmsg, parent, trcbk=False):
+ '''
+ Wrapper method for calibre's error_dialog
+ '''
+ if trcbk:
+ error= ''
+ f=StringIO()
+ print_exc(file=f)
+ error_mess = f.getvalue().splitlines()
+ for line in error_mess:
+ error = error + str(line) + '\n'
+ errmsg = errmsg + '\n\n' + error
+ return error_dialog(parent, _(PLUGIN_NAME + ' v' + PLUGIN_VERSION),
+ _(errmsg), show=True)
+
+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):
+ geom = bytearray(self.saveGeometry())
+ gprefs[self.unique_pref_name] = geom
+ self.persist_custom_prefs()
+
+ def persist_custom_prefs(self):
+ '''
+ Invoked when the dialog is closing. Override this function to call
+ save_custom_pref() if you have a setting you want persisted that you can
+ retrieve in your __init__() using load_custom_pref() when next opened
+ '''
+ pass
+
+ def load_custom_pref(self, name, default=None):
+ return gprefs.get(self.unique_pref_name+':'+name, default)
+
+ def save_custom_pref(self, name, value):
+ gprefs[self.unique_pref_name+':'+name] = value
+
+class ImageTitleLayout(QHBoxLayout):
+ '''
+ A reusable layout widget displaying an image followed by a title
+ '''
+ def __init__(self, parent, icon_name, title):
+ '''
+ :param parent: Parent gui
+ :param icon_name: Path to plugin image resource
+ :param title: String to be displayed beside the image
+ '''
+ QHBoxLayout.__init__(self)
+ self.title_image_label = QLabel(parent)
+ self.update_title_icon(icon_name)
+ self.addWidget(self.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)
+
+ def update_title_icon(self, icon_name):
+ pixmap = get_pixmap(icon_name)
+ if pixmap is None:
+ error_dialog(self.parent(), _('Restart required'),
+ _('Title image not found - you must restart Calibre before using this plugin!'), show=True)
+ else:
+ self.title_image_label.setPixmap(pixmap)
+ self.title_image_label.setMaximumSize(32, 32)
+ self.title_image_label.setScaledContents(True)
+
+
+class ReadOnlyTableWidgetItem(QTableWidgetItem):
+
+ def __init__(self, text):
+ if text is None:
+ text = ''
+ QTableWidgetItem.__init__(self, text, QTableWidgetItem.UserType)
+ self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)