summaryrefslogtreecommitdiffstats
path: root/Obok_plugin
diff options
context:
space:
mode:
authorApprentice Harper <[email protected]>2020-02-16 10:12:25 +0000
committerApprentice Harper <[email protected]>2020-02-16 10:12:25 +0000
commit92bf51bc8f201a2d5b1e8b90b8dc033606dbcfb0 (patch)
tree037f09f49a7d93d6b71dc36db4fba96fa31c5603 /Obok_plugin
parentef3c7f261c9049e7ee861c62f308ed82c5d757fb (diff)
Remove stand-alone apps. Only support the two plugins.
Diffstat (limited to 'Obok_plugin')
-rw-r--r--Obok_plugin/__init__.py76
-rw-r--r--Obok_plugin/action.py497
-rw-r--r--Obok_plugin/common_utils.py590
-rw-r--r--Obok_plugin/config.py218
-rw-r--r--Obok_plugin/dialogs.py455
-rw-r--r--Obok_plugin/images/obok.pngbin0 -> 3155 bytes
-rw-r--r--Obok_plugin/obok/__init__.py4
-rw-r--r--Obok_plugin/obok/legacy_obok.py71
-rw-r--r--Obok_plugin/obok/obok.py760
-rw-r--r--Obok_plugin/obok_dedrm_Help.htm31
-rw-r--r--Obok_plugin/plugin-import-name-obok_dedrm.txt0
-rw-r--r--Obok_plugin/translations/ar.mobin0 -> 2321 bytes
-rw-r--r--Obok_plugin/translations/ar.po331
-rw-r--r--Obok_plugin/translations/de.mobin0 -> 1984 bytes
-rw-r--r--Obok_plugin/translations/de.po102
-rw-r--r--Obok_plugin/translations/default.po335
-rw-r--r--Obok_plugin/translations/es.mobin0 -> 8689 bytes
-rw-r--r--Obok_plugin/translations/es.po419
-rw-r--r--Obok_plugin/translations/nl.mobin0 -> 2026 bytes
-rw-r--r--Obok_plugin/translations/nl.po102
-rw-r--r--Obok_plugin/translations/pt.mobin0 -> 8484 bytes
-rw-r--r--Obok_plugin/translations/pt.po361
-rw-r--r--Obok_plugin/translations/sv.mobin0 -> 8284 bytes
-rw-r--r--Obok_plugin/translations/sv.po366
-rw-r--r--Obok_plugin/utilities.py228
25 files changed, 4946 insertions, 0 deletions
diff --git a/Obok_plugin/__init__.py b/Obok_plugin/__init__.py
new file mode 100644
index 0000000..b593136
--- /dev/null
+++ b/Obok_plugin/__init__.py
@@ -0,0 +1,76 @@
+# 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'
+__version__ = '6.7.0'
+__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 = (6, 7, 0)
+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', 'linux' ]
+ 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_plugin/action.py b/Obok_plugin/action.py
new file mode 100644
index 0000000..a0b63a6
--- /dev/null
+++ b/Obok_plugin/action.py
@@ -0,0 +1,497 @@
+# 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, traceback, 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))
+ #
+ # search for connected device in case serials are saved
+ tmpserials = cfg['kobo_serials']
+ device_path = None
+ try:
+ device = self.parent().device_manager.connected_device
+ if (device):
+ device_path = device._main_prefix
+ debug_print("get_device_settings - device_path=", device_path)
+ else:
+ debug_print("didn't find device")
+ except:
+ debug_print("Exception getting device path. Probably not an E-Ink Kobo device")
+
+ # Get the Kobo Library object (obok v3.01)
+ self.library = KoboLibrary(tmpserials, device_path, cfg['kobo_directory'])
+ debug_print ("got kobodir %s" % self.library.kobodir)
+ if (self.library.kobodir == ''):
+ # linux and no device connected, but could be extended
+ # to the case where on Windows/Mac the prog is not installed
+ msg = _('<p>Could not find Kobo Library\n<p>Windows/Mac: do you have Kobo Desktop installed?\n<p>Windows/Mac/Linux: In case you have an Kobo eInk device, connect the device.')
+ showErrorDlg(msg, None)
+ return
+
+
+ # 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.'))
+ traceback.print_exc()
+ 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_plugin/common_utils.py b/Obok_plugin/common_utils.py
new file mode 100644
index 0000000..0f2164a
--- /dev/null
+++ b/Obok_plugin/common_utils.py
@@ -0,0 +1,590 @@
+#!/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
+from datetime import datetime
+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_plugin/config.py b/Obok_plugin/config.py
new file mode 100644
index 0000000..8244b91
--- /dev/null
+++ b/Obok_plugin/config.py
@@ -0,0 +1,218 @@
+# 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 (Qt, QGroupBox, QListWidget, QLineEdit, QDialogButtonBox, QWidget, QLabel, QDialog, QVBoxLayout, QAbstractItemView, QIcon, QHBoxLayout, QComboBox, QListWidgetItem, QFileDialog)
+except ImportError:
+ from PyQt4.Qt import (Qt, QGroupBox, QListWidget, QLineEdit, QDialogButtonBox, QWidget, QLabel, QDialog, QVBoxLayout, QAbstractItemView, QIcon, QHBoxLayout, QComboBox, QListWidgetItem, QFileDialog)
+
+try:
+ from PyQt5 import Qt as QtGui
+except ImportError:
+ from PyQt4 import QtGui
+
+from calibre.gui2 import (error_dialog, question_dialog, info_dialog, open_url)
+from calibre.utils.config import JSONConfig, config_dir
+
+plugin_prefs = JSONConfig('plugins/obok_dedrm_prefs')
+plugin_prefs.defaults['finding_homes_for_formats'] = 'Ask'
+plugin_prefs.defaults['kobo_serials'] = []
+plugin_prefs.defaults['kobo_directory'] = u''
+
+from calibre_plugins.obok_dedrm.__init__ import PLUGIN_NAME, PLUGIN_VERSION
+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)
+
+ # copy of preferences
+ self.tmpserials = plugin_prefs['kobo_serials']
+ self.kobodirectory = plugin_prefs['kobo_directory']
+
+ 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)
+
+ self.serials_button = QtGui.QPushButton(self)
+ self.serials_button.setToolTip(_(u"Click to manage Kobo serial numbers for Kobo ebooks"))
+ self.serials_button.setText(u"Kobo devices serials")
+ self.serials_button.clicked.connect(self.edit_serials)
+ layout.addWidget(self.serials_button)
+
+ self.kobo_directory_button = QtGui.QPushButton(self)
+ self.kobo_directory_button.setToolTip(_(u"Click to specify the Kobo directory"))
+ self.kobo_directory_button.setText(u"Kobo directory")
+ self.kobo_directory_button.clicked.connect(self.edit_kobo_directory)
+ layout.addWidget(self.kobo_directory_button)
+
+
+ def edit_serials(self):
+ d = ManageKeysDialog(self,u"Kobo device serial number",self.tmpserials, AddSerialDialog)
+ d.exec_()
+
+
+ def edit_kobo_directory(self):
+ tmpkobodirectory = QFileDialog.getExistingDirectory(self, u"Select Kobo directory", self.kobodirectory or "/home", QFileDialog.ShowDirsOnly)
+
+ if tmpkobodirectory != u"" and tmpkobodirectory is not None:
+ self.kobodirectory = tmpkobodirectory
+
+
+ def save_settings(self):
+ plugin_prefs['finding_homes_for_formats'] = unicode(self.find_homes.currentText())
+ plugin_prefs['kobo_serials'] = self.tmpserials
+ plugin_prefs['kobo_directory'] = self.kobodirectory
+
+
+
+
+
+class ManageKeysDialog(QDialog):
+ def __init__(self, parent, key_type_name, plugin_keys, create_key, keyfile_ext = u""):
+ QDialog.__init__(self,parent)
+ self.parent = parent
+ self.key_type_name = key_type_name
+ self.plugin_keys = plugin_keys
+ self.create_key = create_key
+ self.keyfile_ext = keyfile_ext
+ self.json_file = (keyfile_ext == u"k4i")
+
+ self.setWindowTitle("{0} {1}: Manage {2}s".format(PLUGIN_NAME, PLUGIN_VERSION, self.key_type_name))
+
+ # Start Qt Gui dialog layout
+ layout = QVBoxLayout(self)
+ self.setLayout(layout)
+
+ keys_group_box = QGroupBox(_(u"{0}s".format(self.key_type_name)), self)
+ layout.addWidget(keys_group_box)
+ keys_group_box_layout = QHBoxLayout()
+ keys_group_box.setLayout(keys_group_box_layout)
+
+ self.listy = QListWidget(self)
+ self.listy.setToolTip(u"{0}s that will be used to decrypt ebooks".format(self.key_type_name))
+ self.listy.setSelectionMode(QAbstractItemView.SingleSelection)
+ self.populate_list()
+ keys_group_box_layout.addWidget(self.listy)
+
+ button_layout = QVBoxLayout()
+ keys_group_box_layout.addLayout(button_layout)
+ self._add_key_button = QtGui.QToolButton(self)
+ self._add_key_button.setIcon(QIcon(I('plus.png')))
+ self._add_key_button.setToolTip(u"Create new {0}".format(self.key_type_name))
+ self._add_key_button.clicked.connect(self.add_key)
+ button_layout.addWidget(self._add_key_button)
+
+ self._delete_key_button = QtGui.QToolButton(self)
+ self._delete_key_button.setToolTip(_(u"Delete highlighted key"))
+ self._delete_key_button.setIcon(QIcon(I('list_remove.png')))
+ self._delete_key_button.clicked.connect(self.delete_key)
+ button_layout.addWidget(self._delete_key_button)
+
+ spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
+ button_layout.addItem(spacerItem)
+
+ layout.addSpacing(5)
+ migrate_layout = QHBoxLayout()
+ layout.addLayout(migrate_layout)
+ migrate_layout.addStretch()
+ self.button_box = QDialogButtonBox(QDialogButtonBox.Close)
+ self.button_box.rejected.connect(self.close)
+ migrate_layout.addWidget(self.button_box)
+
+ self.resize(self.sizeHint())
+
+ def populate_list(self):
+ if type(self.plugin_keys) == dict:
+ for key in self.plugin_keys.keys():
+ self.listy.addItem(QListWidgetItem(key))
+ else:
+ for key in self.plugin_keys:
+ self.listy.addItem(QListWidgetItem(key))
+
+ def add_key(self):
+ d = self.create_key(self)
+ d.exec_()
+
+ if d.result() != d.Accepted:
+ # New key generation cancelled.
+ return
+ new_key_value = d.key_value
+ if new_key_value in self.plugin_keys:
+ info_dialog(None, "{0} {1}: Duplicate {2}".format(PLUGIN_NAME, PLUGIN_VERSION,self.key_type_name),
+ u"This {0} is already in the list of {0}s has not been added.".format(self.key_type_name), show=True)
+ return
+
+ self.plugin_keys.append(d.key_value)
+ self.listy.clear()
+ self.populate_list()
+
+ def delete_key(self):
+ if not self.listy.currentItem():
+ return
+ keyname = unicode(self.listy.currentItem().text())
+ if not question_dialog(self, "{0} {1}: Confirm Delete".format(PLUGIN_NAME, PLUGIN_VERSION), u"Do you really want to delete the {1} <strong>{0}</strong>?".format(keyname, self.key_type_name), show_copy_button=False, default_yes=False):
+ return
+ self.plugin_keys.remove(keyname)
+
+ self.listy.clear()
+ self.populate_list()
+
+class AddSerialDialog(QDialog):
+ def __init__(self, parent=None,):
+ QDialog.__init__(self, parent)
+ self.parent = parent
+ self.setWindowTitle(u"{0} {1}: Add New eInk Kobo Serial Number".format(PLUGIN_NAME, PLUGIN_VERSION))
+ layout = QVBoxLayout(self)
+ self.setLayout(layout)
+
+ data_group_box = QGroupBox(u"", self)
+ layout.addWidget(data_group_box)
+ data_group_box_layout = QVBoxLayout()
+ data_group_box.setLayout(data_group_box_layout)
+
+ key_group = QHBoxLayout()
+ data_group_box_layout.addLayout(key_group)
+ key_group.addWidget(QLabel(u"EInk Kobo Serial Number:", self))
+ self.key_ledit = QLineEdit("", self)
+ self.key_ledit.setToolTip(u"Enter an eInk Kobo serial number. EInk Kobo serial numbers are 13 characters long and usually start with a 'N'. Kobo Serial Numbers are case-sensitive, so be sure to enter the upper and lower case letters unchanged.")
+ key_group.addWidget(self.key_ledit)
+
+ self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
+ self.button_box.accepted.connect(self.accept)
+ self.button_box.rejected.connect(self.reject)
+ layout.addWidget(self.button_box)
+
+ self.resize(self.sizeHint())
+
+ @property
+ def key_name(self):
+ return unicode(self.key_ledit.text()).strip()
+
+ @property
+ def key_value(self):
+ return unicode(self.key_ledit.text()).strip()
+
+ def accept(self):
+ if len(self.key_name) == 0 or self.key_name.isspace():
+ errmsg = u"Please enter an eInk Kindle Serial Number or click Cancel in the dialog."
+ return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
+ if len(self.key_name) != 13:
+ errmsg = u"EInk Kobo Serial Numbers must be 13 characters long. This is {0:d} characters long.".format(len(self.key_name))
+ return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
+ QDialog.accept(self)
diff --git a/Obok_plugin/dialogs.py b/Obok_plugin/dialogs.py
new file mode 100644
index 0000000..85abfaf
--- /dev/null
+++ b/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_plugin/images/obok.png b/Obok_plugin/images/obok.png
new file mode 100644
index 0000000..0f95715
--- /dev/null
+++ b/Obok_plugin/images/obok.png
Binary files differ
diff --git a/Obok_plugin/obok/__init__.py b/Obok_plugin/obok/__init__.py
new file mode 100644
index 0000000..7da8f17
--- /dev/null
+++ b/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_plugin/obok/legacy_obok.py b/Obok_plugin/obok/legacy_obok.py
new file mode 100644
index 0000000..f105688
--- /dev/null
+++ b/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_plugin/obok/obok.py b/Obok_plugin/obok/obok.py
new file mode 100644
index 0000000..21ef14a
--- /dev/null
+++ b/Obok_plugin/obok/obok.py
@@ -0,0 +1,760 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Version 3.2.5 December 2016
+# Improve detection of good text decryption.
+#
+# Version 3.2.4 December 2016
+# Remove incorrect support for Kobo Desktop under Wine
+#
+# Version 3.2.3 October 2016
+# Fix for windows network user and more xml fixes
+#
+# Version 3.2.2 October 2016
+# Change to the way the new database version is handled.
+#
+# Version 3.2.1 September 2016
+# Update for v4.0 of Windows Desktop app.
+#
+# Version 3.2.0 January 2016
+# Update for latest version of Windows Desktop app.
+# Support Kobo devices in the command line version.
+#
+# Version 3.1.9 November 2015
+# Handle Kobo Desktop under wine on Linux
+#
+# Version 3.1.8 November 2015
+# Handle the case of Kobo Arc or Vox device (i.e. don't crash).
+#
+# Version 3.1.7 October 2015
+# Handle the case of no device or database more gracefully.
+#
+# Version 3.1.6 September 2015
+# Enable support for Kobo devices
+# More character encoding fixes (unicode strings)
+#
+# Version 3.1.5 September 2015
+# Removed requirement that a purchase has been made.
+# Also add in character encoding fixes
+#
+# Version 3.1.4 September 2015
+# Updated for version 3.17 of the Windows Desktop app.
+#
+# Version 3.1.3 August 2015
+# Add translations for Portuguese and Arabic
+#
+# Version 3.1.2 January 2015
+# Add coding, version number and version announcement
+#
+# 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."""
+from __future__ import print_function
+
+__version__ = '3.2.4'
+__about__ = u"Obok v{0}\nCopyright © 2012-2016 Physisticated et al.".format(__version__)
+
+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
+import argparse
+import tempfile
+
+can_parse_xml = True
+try:
+ from xml.etree import ElementTree as ET
+ # print u"using xml.etree for xml parsing"
+except ImportError:
+ can_parse_xml = False
+ # print u"Cannot find xml.etree, disabling extraction of serial numbers"
+
+# List of all known hash keys
+KOBO_HASH_KEYS = ['88b3a2e13', 'XzUhGYdFp', 'NoCanLook','QJhwzAtXL']
+
+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()
+
+# Wrap a stream so that output gets flushed immediately
+# and also make sure that any unicode strings get
+# encoded using "replace" before writing them.
+class SafeUnbuffered:
+ def __init__(self, stream):
+ self.stream = stream
+ self.encoding = stream.encoding
+ if self.encoding == None:
+ self.encoding = "utf-8"
+ def write(self, data):
+ if isinstance(data,unicode):
+ data = data.encode(self.encoding,"replace")
+ self.stream.write(data)
+ self.stream.flush()
+ def __getattr__(self, attr):
+ return getattr(self.stream, attr)
+
+
+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, serials = [], device_path = None, desktopkobodir = u""):
+ print(__about__)
+ self.kobodir = u""
+ kobodb = u""
+
+ # Order of checks
+ # 1. first check if a device_path has been passed in, and whether
+ # we can find the sqlite db in the respective place
+ # 2. if 1., and we got some serials passed in (from saved
+ # settings in calibre), just use it
+ # 3. if 1. worked, but we didn't get serials, try to parse them
+ # from the device, if this didn't work, unset everything
+ # 4. if by now we don't have kobodir set, give up on device and
+ # try to use the Desktop app.
+
+ # step 1. check whether this looks like a real device
+ if (device_path):
+ # we got a device path
+ self.kobodir = os.path.join(device_path, u".kobo")
+ # devices use KoboReader.sqlite
+ kobodb = os.path.join(self.kobodir, u"KoboReader.sqlite")
+ if (not(os.path.isfile(kobodb))):
+ # device path seems to be wrong, unset it
+ device_path = u""
+ self.kobodir = u""
+ kobodb = u""
+
+ if (self.kobodir):
+ # step 3. we found a device but didn't get serials, try to get them
+ if (len(serials) == 0):
+ # we got a device path but no saved serial
+ # try to get the serial from the device
+ # print u"get_device_settings - device_path = {0}".format(device_path)
+ # get serial from device_path/.adobe-digital-editions/device.xml
+ if can_parse_xml:
+ devicexml = os.path.join(device_path, '.adobe-digital-editions', 'device.xml')
+ # print u"trying to load {0}".format(devicexml)
+ if (os.path.exists(devicexml)):
+ # print u"trying to parse {0}".format(devicexml)
+ xmltree = ET.parse(devicexml)
+ for node in xmltree.iter():
+ if "deviceSerial" in node.tag:
+ serial = node.text
+ # print u"found serial {0}".format(serial)
+ serials.append(serial)
+ break
+ else:
+ # print u"cannot get serials from device."
+ device_path = u""
+ self.kobodir = u""
+ kobodb = u""
+
+ if (self.kobodir == u""):
+ # step 4. we haven't found a device with serials, so try desktop apps
+ if desktopkobodir != u'':
+ self.kobodir = desktopkobodir
+
+ if (self.kobodir == u""):
+ if sys.platform.startswith('win'):
+ import _winreg as winreg
+ if sys.getwindowsversion().major > 5:
+ if 'LOCALAPPDATA' in os.environ.keys():
+ # Python 2.x does not return unicode env. Use Python 3.x
+ self.kobodir = winreg.ExpandEnvironmentStrings(u"%LOCALAPPDATA%")
+ if (self.kobodir == u""):
+ if 'USERPROFILE' in os.environ.keys():
+ # Python 2.x does not return unicode env. Use Python 3.x
+ self.kobodir = os.path.join(winreg.ExpandEnvironmentStrings(u"%USERPROFILE%"), u"Local Settings", u"Application Data")
+ self.kobodir = os.path.join(self.kobodir, u"Kobo", u"Kobo Desktop Edition")
+ elif sys.platform.startswith('darwin'):
+ self.kobodir = os.path.join(os.environ['HOME'], u"Library", u"Application Support", u"Kobo", u"Kobo Desktop Edition")
+ #elif linux_path != None:
+ # Probably Linux, let's get the wine prefix and path to Kobo.
+ # self.kobodir = os.path.join(linux_path, u"Local Settings", u"Application Data", u"Kobo", u"Kobo Desktop Edition")
+ # desktop versions use Kobo.sqlite
+ kobodb = os.path.join(self.kobodir, u"Kobo.sqlite")
+ # check for existence of file
+ if (not(os.path.isfile(kobodb))):
+ # give up here, we haven't found anything useful
+ self.kobodir = u""
+ kobodb = u""
+
+ if (self.kobodir != u""):
+ self.bookdir = os.path.join(self.kobodir, u"kepub")
+ # make a copy of the database in a temporary file
+ # so we can ensure it's not using WAL logging which sqlite3 can't do.
+ self.newdb = tempfile.NamedTemporaryFile(mode='wb', delete=False)
+ print(self.newdb.name)
+ olddb = open(kobodb, 'rb')
+ self.newdb.write(olddb.read(18))
+ self.newdb.write('\x01\x01')
+ olddb.read(2)
+ self.newdb.write(olddb.read())
+ olddb.close()
+ self.newdb.close()
+ self.__sqlite = sqlite3.connect(self.newdb.name)
+ self.__cursor = self.__sqlite.cursor()
+ self._userkeys = []
+ self._books = []
+ self._volumeID = []
+ self._serials = serials
+
+ def close (self):
+ """Closes the database used by the library."""
+ self.__cursor.close()
+ self.__sqlite.close()
+ # delete the temporary copy of the database
+ os.remove(self.newdb.name)
+
+ @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
+ for macaddr in self.__getmacaddrs():
+ self._userkeys.extend(self.__getuserkeys(macaddr))
+ 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, u"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)
+ (p_in, p_out, p_err) = os.popen3('ipconfig /all')
+ for line in p_out:
+ 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 u"m:{0}".format(m[0])
+ macaddrs.append(m[0].upper())
+ else:
+ # probably linux
+
+ # let's try ip
+ c = re.compile('\s(' + '[0-9a-f]{2}:' * 5 + '[0-9a-f]{2})(\s|$)', re.IGNORECASE)
+ for line in os.popen('ip -br link'):
+ m = c.search(line)
+ if m:
+ macaddrs.append(m.group(1).upper())
+
+ # let's try ipconfig under wine
+ 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())
+
+ # extend the list of macaddrs in any case with the serials
+ # cannot hurt ;-)
+ macaddrs.extend(self._serials)
+
+ return macaddrs
+
+ def __getuserids (self):
+ userids = []
+ cursor = self.__cursor.execute('SELECT UserID FROM user')
+ row = cursor.fetchone()
+ while row is not None:
+ try:
+ userid = row[0]
+ userids.append(userid)
+ except:
+ pass
+ row = cursor.fetchone()
+ return userids
+
+ def __getuserkeys (self, macaddr):
+ userids = self.__getuserids()
+ userkeys = []
+ for hash in KOBO_HASH_KEYS:
+ deviceid = hashlib.sha256(hash + macaddr).hexdigest()
+ for userid in userids:
+ userkey = hashlib.sha256(deviceid + userid).hexdigest()
+ userkeys.append(binascii.a2b_hex(userkey[32:]))
+ return userkeys
+
+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':
+ # assume utf-8 with no BOM
+ textoffset = 0
+ stride = 1
+ print(u"Checking text:{0}:".format(contents[:10]))
+ # check for byte order mark
+ if contents[:3]=="\xef\xbb\xbf":
+ # seems to be utf-8 with BOM
+ print(u"Could be utf-8 with BOM")
+ textoffset = 3
+ elif contents[:2]=="\xfe\xff":
+ # seems to be utf-16BE
+ print(u"Could be utf-16BE")
+ textoffset = 3
+ stride = 2
+ elif contents[:2]=="\xff\xfe":
+ # seems to be utf-16LE
+ print(u"Could be utf-16LE")
+ textoffset = 2
+ stride = 2
+ else:
+ print(u"Perhaps utf-8 without BOM")
+
+ # now check that the first few characters are in the ASCII range
+ for i in xrange(textoffset,textoffset+5*stride,stride):
+ if ord(contents[i])<32 or ord(contents[i])>127:
+ # Non-ascii, so decryption probably failed
+ print(u"Bad character at {0}, value {1}".format(i,ord(contents[i])))
+ raise ValueError
+ print(u"Seems to be good text")
+ return True
+ if contents[:5]=="<?xml" or contents[:8]=="\xef\xbb\xbf<?xml":
+ # utf-8
+ return True
+ elif contents[:14]=="\xfe\xff\x00<\x00?\x00x\x00m\x00l":
+ # utf-16BE
+ return True
+ elif contents[:14]=="\xff\xfe<\x00?\x00x\x00m\x00l\x00":
+ # utf-16LE
+ return True
+ elif contents[:9]=="<!DOCTYPE" or contents[:12]=="\xef\xbb\xbf<!DOCTYPE":
+ # utf-8 of weird <!DOCTYPE start
+ return True
+ elif contents[:22]=="\xfe\xff\x00<\x00!\x00D\x00O\x00C\x00T\x00Y\x00P\x00E":
+ # utf-16BE of weird <!DOCTYPE start
+ return True
+ elif contents[:22]=="\xff\xfe<\x00!\x00D\x00O\x00C\x00T\x00Y\x00P\x00E\x00":
+ # utf-16LE of weird <!DOCTYPE start
+ return True
+ else:
+ print(u"Bad XML: {0}".format(contents[:8]))
+ raise ValueError
+ elif self.mimetype == 'image/jpeg':
+ if contents[:3] == '\xff\xd8\xff':
+ return True
+ else:
+ print(u"Bad JPEG: {0}".format(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
+
+def decrypt_book(book, lib):
+ print(u"Converting {0}".format(book.title))
+ zin = zipfile.ZipFile(book.filename, "r")
+ # make filename out of Unicode alphanumeric and whitespace equivalents from title
+ outname = u"{0}.epub".format(re.sub('[^\s\w]', '_', book.title, 0, re.UNICODE))
+ if (book.type == 'drm-free'):
+ print(u"DRM-free book, conversion is not needed")
+ shutil.copyfile(book.filename, outname)
+ print(u"Book saved as {0}".format(os.path.join(os.getcwd(), outname)))
+ return 0
+ result = 1
+ for userkey in lib.userkeys:
+ print(u"Trying key: {0}".format(userkey.encode('hex_codec')))
+ 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.
+ file.check(contents)
+ zout.writestr(filename, contents)
+ zout.close()
+ print(u"Decryption succeeded.")
+ print(u"Book saved as {0}".format(os.path.join(os.getcwd(), outname)))
+ result = 0
+ break
+ except ValueError:
+ print(u"Decryption failed.")
+ zout.close()
+ os.remove(outname)
+ zin.close()
+ return result
+
+
+def cli_main():
+ description = __about__
+ epilog = u"Parsing of arguments failed."
+ parser = argparse.ArgumentParser(prog=sys.argv[0], description=description, epilog=epilog)
+ parser.add_argument('--devicedir', default='/media/KOBOeReader', help="directory of connected Kobo device")
+ parser.add_argument('--all', action='store_true', help="flag for converting all books on device")
+ args = vars(parser.parse_args())
+ serials = []
+ devicedir = u""
+ if args['devicedir']:
+ devicedir = args['devicedir']
+
+ lib = KoboLibrary(serials, devicedir)
+
+ if args['all']:
+ books = lib.books
+ else:
+ for i, book in enumerate(lib.books):
+ print(u"{0}: {1}".format(i + 1, book.title))
+ print(u"Or 'all'")
+
+ choice = raw_input(u"Convert book number... ")
+ if choice == u'all':
+ books = list(lib.books)
+ else:
+ try:
+ num = int(choice)
+ books = [lib.books[num - 1]]
+ except (ValueError, IndexError):
+ print(u"Invalid choice. Exiting...")
+ exit()
+
+ results = [decrypt_book(book, lib) for book in books]
+ lib.close()
+ overall_result = all(result != 0 for result in results)
+ if overall_result != 0:
+ print(u"Could not decrypt book with any of the keys found.")
+ return overall_result
+
+
+if __name__ == '__main__':
+ sys.stdout=SafeUnbuffered(sys.stdout)
+ sys.stderr=SafeUnbuffered(sys.stderr)
+ sys.exit(cli_main())
diff --git a/Obok_plugin/obok_dedrm_Help.htm b/Obok_plugin/obok_dedrm_Help.htm
new file mode 100644
index 0000000..251c23f
--- /dev/null
+++ b/Obok_plugin/obok_dedrm_Help.htm
@@ -0,0 +1,31 @@
+<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.3)</h3>
+
+<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 Apprentice Alf's blog.</p>
+
+</body>
+
+</html>
diff --git a/Obok_plugin/plugin-import-name-obok_dedrm.txt b/Obok_plugin/plugin-import-name-obok_dedrm.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/Obok_plugin/plugin-import-name-obok_dedrm.txt
diff --git a/Obok_plugin/translations/ar.mo b/Obok_plugin/translations/ar.mo
new file mode 100644
index 0000000..401a78e
--- /dev/null
+++ b/Obok_plugin/translations/ar.mo
Binary files differ
diff --git a/Obok_plugin/translations/ar.po b/Obok_plugin/translations/ar.po
new file mode 100644
index 0000000..7c09de8
--- /dev/null
+++ b/Obok_plugin/translations/ar.po
@@ -0,0 +1,331 @@
+# 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: \n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-17 12:51+0100\n"
+"PO-Revision-Date: 2015-05-31 22:44+1000\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Last-Translator: \n"
+"Language-Team: \n"
+"X-Generator: Poedit 1.8.1\n"
+"Plural-Forms: nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 "
+"&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);\n"
+"Language: ar\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>لا يوجد كتب بمكتبة كوبو الخاصة بكم\n"
+"هل أنت متأكد أنها موجودة\\معدة بشكل سليم\\محملة بشكل كامل؟"
+
+#: 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 "<p>{0} تم إضافتهم بنجاح."
+
+#: 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>لم يتم نقل كل كتب كوبو إلى كاليبر.<br/><br/>شاهد التقرير لمزيد من التفاصيل"
+
+#: 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 "<p>لم يتم إضافة {0} بسبب {1}"
+
+#: 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:65
+msgid "Obok DeDRM"
+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 "خطأ فى تحميل مفتاح AES"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:175
+msgid "AES decryption failed"
+msgstr "خطأ فى فك الحماية بطريقة AES"
diff --git a/Obok_plugin/translations/de.mo b/Obok_plugin/translations/de.mo
new file mode 100644
index 0000000..7fc5ef4
--- /dev/null
+++ b/Obok_plugin/translations/de.mo
Binary files differ
diff --git a/Obok_plugin/translations/de.po b/Obok_plugin/translations/de.po
new file mode 100644
index 0000000..05b6d4b
--- /dev/null
+++ b/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_plugin/translations/default.po b/Obok_plugin/translations/default.po
new file mode 100644
index 0000000..2b73c84
--- /dev/null
+++ b/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_plugin/translations/es.mo b/Obok_plugin/translations/es.mo
new file mode 100644
index 0000000..e4d2a3a
--- /dev/null
+++ b/Obok_plugin/translations/es.mo
Binary files differ
diff --git a/Obok_plugin/translations/es.po b/Obok_plugin/translations/es.po
new file mode 100644
index 0000000..6ca0718
--- /dev/null
+++ b/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_plugin/translations/nl.mo b/Obok_plugin/translations/nl.mo
new file mode 100644
index 0000000..78b6f53
--- /dev/null
+++ b/Obok_plugin/translations/nl.mo
Binary files differ
diff --git a/Obok_plugin/translations/nl.po b/Obok_plugin/translations/nl.po
new file mode 100644
index 0000000..24490c3
--- /dev/null
+++ b/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_plugin/translations/pt.mo b/Obok_plugin/translations/pt.mo
new file mode 100644
index 0000000..5fdea4b
--- /dev/null
+++ b/Obok_plugin/translations/pt.mo
Binary files differ
diff --git a/Obok_plugin/translations/pt.po b/Obok_plugin/translations/pt.po
new file mode 100644
index 0000000..43e3c5b
--- /dev/null
+++ b/Obok_plugin/translations/pt.po
@@ -0,0 +1,361 @@
+# 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: \n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-17 12:51+0100\n"
+"PO-Revision-Date: 2015-05-31 22:44+1000\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Last-Translator: \n"
+"Language-Team: \n"
+"X-Generator: Poedit 1.8.1\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"Language: pt\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>Não foram encontrados livros na livraria Kobo\n"
+"Tem a certeza de que está instalado\\configured\\synchronized?"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:87
+msgid "Legacy key found: "
+msgstr "Chave de legado encontrada"
+
+#: 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 na obtenção das chaves com o novo 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 "Encontradas {0} chaves possíveis para experimentar."
+
+#: 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 ""
+"Não foi encontrada nenhuma chave de usuário com a qual desencriptar os "
+"livros. Não vale a pena continuar."
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:115
+msgid "{} - Decryption canceled by user."
+msgstr "Desencriptação cancelada pelo utilizador."
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:135
+msgid "{} - \"Add books\" canceled by user."
+msgstr "{} - \"Adição de livros\" cancelada pelo utilizador."
+
+#: 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 "{} - finalizando os 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 "O utilizador optou por não tentar inserir formatos EPUB"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:188
+msgid "{0} - Decrypting {1}"
+msgstr "Desencriptando"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:197
+msgid "{0} - Couldn't decrypt {1}"
+msgstr "{0} - Não foi possível desencriptar {1}"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:198
+msgid "decryption errors"
+msgstr "erros na desencriptação"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:213
+msgid "{0} - Added {1}"
+msgstr "{0} - Adicionado {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} já existe. a adição do formato irá ser tentada mais tarde."
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:219
+msgid "duplicate detected"
+msgstr "detectados duplicados"
+
+#: 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 adicionado com sucesso ao existente {1}"
+
+#: 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} - Erro ao adicionar o formato EPUB ao existente {1}. Isto realmente não "
+"deveria acontecer."
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:259
+msgid "{} - \"Insert formats\" canceled by user."
+msgstr "{} - \"Inserção de formatos\" cancelada pelo utilizador."
+
+#: 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} adicionado com sucesso à 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 ""
+"não adicionados porque foram detectados com o mesmo título/autor.<br /><br /"
+">Gostaria de tentar e adicionar o formato EPUB{0}"
+
+#: 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 ""
+" às entradas existentes?<br /><br />NOTA: EPUBs pré existentes não serão "
+"reescritos."
+
+#: 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} -- não adicionado porque {1} na 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> -- não adicionado porque {1} na 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 ""
+"Gostaria de tentar adicionar o formato EPUB a um duplicado já existente no "
+"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: EPUBs pré existentes não serão reescritos."
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:346
+msgid "Trying key: "
+msgstr "Experimentando a chave:"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:378
+msgid "Decryption failed, trying next key."
+msgstr "A desencriptação falhou, tentado a próxima chave."
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:382
+msgid "Unknown Error decrypting, trying next key.."
+msgstr "Erro desconhecido na desencriptação, tentado a próxima chave."
+
+#: 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 os livros Kobo selecionados foram adicionados como livros novos no "
+"calibre ou inseridos em livros já existentes no calibre.<br /><br />Sem "
+"problemas."
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:399
+msgid "<p>{0} successfully added."
+msgstr "<p>{0} adicionados com sucesso."
+
+#: 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>Nem todos os livros Kobo selecionados seguiram para o calibre.<br /><br /"
+">Veja o relatório para mais detalhes."
+
+#: 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>tentativas totais:</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>Erros de desencriptação:</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>Novos livros criados:</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 não adicionados:</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>Importação de livros cancelada pelo utilizador:</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>Novos formatos EPUB inseridos em livros existentes no 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 NÃO inseridos em livros existentes no calibre:</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 ""
+"(Porque o utilizador <i>escolheu</i> não os inserir, ou porque todos os "
+"duplicados já tinham um 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>Importação do formato cancelada pelo utilizador:</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 do livro desconhecido"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:460
+msgid "it couldn't be decrypted."
+msgstr "não pode ser desencriptado."
+
+#: 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 ""
+"o utilizador ESCOLHEU não inserir o novo formato EPUB, ou todas as entradas "
+"existentes no calibre já tinham um 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 "de razões desconhecidas. Estou envergonhado!"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:465
+msgid "<p>{0} not added because {1}"
+msgstr "<p>{0} não adicionado porque {1}"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:226
+msgid "Help"
+msgstr "Help"
+
+#: 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 "Reinicio requerido"
+
+#: 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 ""
+"Imagem do título não encontrada - tem que reiniciar o Calibre antes de "
+"utilizar este plugin!"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:322
+msgid "Undefined"
+msgstr "Não definido"
+
+#: 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 ""
+"Quando deve o Obok tentar inserir EPUBs em entradas já existentes no calibre?"
+
+#: 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>Comportamento por defeito quando são detetados duplicados. Nenhuma das "
+"escolhas fará com que os livros existentes no calibre sejam reescritos"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:35
+msgid "Ask"
+msgstr "Pergunta"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:35
+msgid "Always"
+msgstr "Sempre"
+
+#: 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:65
+msgid "Obok DeDRM"
+msgstr ""
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:89
+msgid "Select All"
+msgstr "Selecionar todos"
+
+#: 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 "Selecionar todos os livros para adicioná-los à biblioteca do calibre."
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:92
+msgid "All with DRM"
+msgstr "Todos com DRM"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:93
+msgid "Select all books with DRM."
+msgstr "Selecionar todos os livros com DRM"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:95
+msgid "All DRM free"
+msgstr "Todos sem DRM"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:96
+msgid "Select all books without DRM."
+msgstr "Selecionar todos os livros sem 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 "Série"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:369
+msgid "Copy to clipboard"
+msgstr "Copiar para a área de transferência"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:397
+msgid "View Report"
+msgstr "Ver relatório"
+
+#: 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 "Remove o DRM dos kepubs Kobo e adiciona-os à biblioteca."
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:162
+msgid "AES improper key used"
+msgstr "AES chave imprópria usada"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:167
+msgid "Failed to initialize AES key"
+msgstr "Falha na inicialização da chave AES"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:175
+msgid "AES decryption failed"
+msgstr "A desencriptação da chave AES falhou"
diff --git a/Obok_plugin/translations/sv.mo b/Obok_plugin/translations/sv.mo
new file mode 100644
index 0000000..235c9da
--- /dev/null
+++ b/Obok_plugin/translations/sv.mo
Binary files differ
diff --git a/Obok_plugin/translations/sv.po b/Obok_plugin/translations/sv.po
new file mode 100644
index 0000000..66b14ab
--- /dev/null
+++ b/Obok_plugin/translations/sv.po
@@ -0,0 +1,366 @@
+# 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: \n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-17 12:51+0100\n"
+"PO-Revision-Date: 2020-02-02 09:18+0100\n"
+"Language: sv\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"
+"Last-Translator: \n"
+"Language-Team: \n"
+"X-Generator: Poedit 2.2.4\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>Inga böcker finns i Kobo-bibliotek\n"
+"Är du säker på att den är installerad\\konfigurerad\\synkroniserad?"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:87
+msgid "Legacy key found: "
+msgstr "Äldre nyckel hittades: "
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:93
+msgid "Trouble retrieving keys with newer obok method."
+msgstr "Problem med att hämta nycklar med nyare obok-metod."
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:97
+msgid "Found {0} possible keys to try."
+msgstr "Hittade {0} möjliga nycklar att pröva med."
+
+#: 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>Inga användarnycklar hittades för att dekryptera böcker med. Det är ingen "
+"idé att fortsätta."
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:115
+msgid "{} - Decryption canceled by user."
+msgstr "{} - Dekryptering avbryts av användaren."
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:135
+msgid "{} - \"Add books\" canceled by user."
+msgstr "{} - \"Lägg till böcker\" avbröts av användaren."
+
+#: 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 "{} - samlar in resultat."
+
+#: 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 "{} - Användaren valde att inte försöka infoga EPUB-format"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:188
+msgid "{0} - Decrypting {1}"
+msgstr "{0} - Dekrypterar {1}"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:197
+msgid "{0} - Couldn't decrypt {1}"
+msgstr "{0} - Kunde inte dekryptera {1}"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:198
+msgid "decryption errors"
+msgstr "dekrypteringsfel"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:213
+msgid "{0} - Added {1}"
+msgstr "{0} - Lade till {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} finns redan. Kommer att försöka lägga till format senare."
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:219
+msgid "duplicate detected"
+msgstr "dubblett upptäcktes"
+
+#: 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} - Lade till EPUB-format till befintliga {1}"
+
+#: 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} - Fel vid tillägg av EPUB-format till befintligt {1}. Det här borde inte "
+"hända."
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:259
+msgid "{} - \"Insert formats\" canceled by user."
+msgstr "{} - \"Infoga format\" avbröts av användaren."
+
+#: 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} lades till bibliotek.<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 ""
+"inte tillagd eftersom böcker med samma titel/författare upptäcktes.<br/><br /"
+">Vill du försöka lägga till EPUB-formatet{0}"
+
+#: 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 ""
+" till dessa befintliga poster?<br /><br />OBS: inga befintliga EPUB:er "
+"kommer att skrivas över."
+
+#: 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} -- inte tillagd på grund av {1} i ditt bibliotek.\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> -- inte tillagd på grund av {1} i ditt bibliotek.<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 ""
+"Vill du försöka lägga till EPUB-formatet till en tillgänglig calibre-"
+"dubblett?<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 "OBS: ingen befintlig EPUB kommer att skrivas över."
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:346
+msgid "Trying key: "
+msgstr "Prövar nyckel: "
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:378
+msgid "Decryption failed, trying next key."
+msgstr "Det gick inte att dekryptera, prövar nästa nyckel."
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:382
+msgid "Unknown Error decrypting, trying next key.."
+msgstr "Okänt fel dekryptering, prövar nästa nyckel.."
+
+#: 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>Alla valda Kobo-böcker läggs till som nya calibre-böcker eller infogas i "
+"befintliga calibre-e-böcker.<br /><br />Inga problem."
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:399
+msgid "<p>{0} successfully added."
+msgstr "<p>{0} har lagts till."
+
+#: 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>Inte alla valda Kobo-böcker lades till i calibre.<br /><br />Visa rapport "
+"för detaljer."
+
+#: 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>Försök totalt:</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>Dekrypteringsfel:</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>Nya böcker skapade:</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>Dubbletter som inte tillsattes:</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>Bokimport avbröts av användaren:</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>Nya EPUB-format infogade i befintliga calibre-böcker:</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>EPUB-format som INTE infogats i befintliga calibre-böcker:</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 ""
+"(Antingen för att användaren <i>valde</i> att inte infoga dem, eller för att "
+"alla dubbletter redan hade ett EPUB-format)"
+
+#: 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>Format-import avbröts av användaren:</b> {}</p>\n"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:458
+msgid "Unknown Book Title"
+msgstr "Okänd boktitel"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:460
+msgid "it couldn't be decrypted."
+msgstr "den kunde inte dekrypteras."
+
+#: 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 ""
+"användaren VALDE att inte infoga det nya EPUB-formatet, eller alla "
+"befintliga calibre-poster hade redan ett EPUB-format."
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:464
+msgid "of unknown reasons. Gosh I'm embarrassed!"
+msgstr "av okända skäl. Jag skäms!"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:465
+msgid "<p>{0} not added because {1}"
+msgstr "<p>{0} inte tillagd eftersom {1}"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:226
+msgid "Help"
+msgstr "Hjälp"
+
+#: 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 "Omstart krävs"
+
+#: 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 ""
+"Titelbild hittades inte - du måste starta calibre innan du använder denna "
+"insticksmodul!"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:322
+msgid "Undefined"
+msgstr "Obestämd"
+
+#: 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 "När ska Obok försöka infoga EPUB:er i befintliga calibre-böcker?"
+
+#: 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>Standardbeteende när dubbletter upptäcks. Inget av alternativen kommer "
+"att orsaka calibre-e-böcker att skrivas över"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:35
+msgid "Ask"
+msgstr "Fråga"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:35
+msgid "Always"
+msgstr "Alltid"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:35
+msgid "Never"
+msgstr "Aldrig"
+
+#: 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/\">Hjälp</a>"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:89
+msgid "Select All"
+msgstr "Välj alla"
+
+#: 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 "Välj alla böcker för att lägga till dem i calibre-biblioteket."
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:92
+msgid "All with DRM"
+msgstr "Alla med DRM"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:93
+msgid "Select all books with DRM."
+msgstr "Välj alla böcker med DRM."
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:95
+msgid "All DRM free"
+msgstr "Alla DRM fria"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:96
+msgid "Select all books without DRM."
+msgstr "Välj alla böcker utan DRM."
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:146
+msgid "Title"
+msgstr "Titel"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:146
+msgid "Author"
+msgstr "Författare"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:146
+msgid "Series"
+msgstr "Serier"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:369
+msgid "Copy to clipboard"
+msgstr "Kopiera till urklipp"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:397
+msgid "View Report"
+msgstr "Visa rapport"
+
+#: 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 "Tar bort DRM från Kobo-kepubs och lägger till dem i biblioteket."
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:162
+msgid "AES improper key used"
+msgstr "AES felaktig nyckel används"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:167
+msgid "Failed to initialize AES key"
+msgstr "Det gick inte att initiera AES-nyckel"
+
+#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:175
+msgid "AES decryption failed"
+msgstr "AES dekryptering misslyckades"
diff --git a/Obok_plugin/utilities.py b/Obok_plugin/utilities.py
new file mode 100644
index 0000000..62e305c
--- /dev/null
+++ b/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)