obok 3.1.1 plugin unzipped
parent
9d9c879413
commit
cf922b6ba1
@ -0,0 +1,75 @@
|
|||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
from __future__ import (unicode_literals, division, absolute_import,
|
||||||
|
print_function)
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
#####################################################################
|
||||||
|
# Plug-in base class
|
||||||
|
#####################################################################
|
||||||
|
|
||||||
|
from calibre.customize import InterfaceActionBase
|
||||||
|
|
||||||
|
try:
|
||||||
|
load_translations()
|
||||||
|
except NameError:
|
||||||
|
pass # load_translations() added in calibre 1.9
|
||||||
|
|
||||||
|
PLUGIN_NAME = 'Obok DeDRM'
|
||||||
|
PLUGIN_SAFE_NAME = PLUGIN_NAME.strip().lower().replace(' ', '_')
|
||||||
|
PLUGIN_DESCRIPTION = _('Removes DRM from Kobo kepubs and adds them to the library.')
|
||||||
|
PLUGIN_VERSION_TUPLE = (3, 1, 1)
|
||||||
|
PLUGIN_VERSION = '.'.join([str(x) for x in PLUGIN_VERSION_TUPLE])
|
||||||
|
HELPFILE_NAME = PLUGIN_SAFE_NAME + '_Help.htm'
|
||||||
|
PLUGIN_AUTHORS = 'Anon'
|
||||||
|
#####################################################################
|
||||||
|
|
||||||
|
class ObokDeDRMAction(InterfaceActionBase):
|
||||||
|
|
||||||
|
name = PLUGIN_NAME
|
||||||
|
description = PLUGIN_DESCRIPTION
|
||||||
|
supported_platforms = ['windows', 'osx']
|
||||||
|
author = PLUGIN_AUTHORS
|
||||||
|
version = PLUGIN_VERSION_TUPLE
|
||||||
|
minimum_calibre_version = (1, 0, 0)
|
||||||
|
|
||||||
|
#: This field defines the GUI plugin class that contains all the code
|
||||||
|
#: that actually does something. Its format is module_path:class_name
|
||||||
|
#: The specified class must be defined in the specified module.
|
||||||
|
actual_plugin = 'calibre_plugins.'+PLUGIN_SAFE_NAME+'.action:InterfacePluginAction'
|
||||||
|
|
||||||
|
def is_customizable(self):
|
||||||
|
'''
|
||||||
|
This method must return True to enable customization via
|
||||||
|
Preferences->Plugins
|
||||||
|
'''
|
||||||
|
return True
|
||||||
|
|
||||||
|
def config_widget(self):
|
||||||
|
'''
|
||||||
|
Implement this method and :meth:`save_settings` in your plugin to
|
||||||
|
use a custom configuration dialog.
|
||||||
|
|
||||||
|
This method, if implemented, must return a QWidget. The widget can have
|
||||||
|
an optional method validate() that takes no arguments and is called
|
||||||
|
immediately after the user clicks OK. Changes are applied if and only
|
||||||
|
if the method returns True.
|
||||||
|
|
||||||
|
If for some reason you cannot perform the configuration at this time,
|
||||||
|
return a tuple of two strings (message, details), these will be
|
||||||
|
displayed as a warning dialog to the user and the process will be
|
||||||
|
aborted.
|
||||||
|
|
||||||
|
The base class implementation of this method raises NotImplementedError
|
||||||
|
so by default no user configuration is possible.
|
||||||
|
'''
|
||||||
|
if self.actual_plugin_:
|
||||||
|
from calibre_plugins.obok_dedrm.config import ConfigWidget
|
||||||
|
return ConfigWidget(self.actual_plugin_)
|
||||||
|
|
||||||
|
def save_settings(self, config_widget):
|
||||||
|
'''
|
||||||
|
Save the settings specified by the user with config_widget.
|
||||||
|
'''
|
||||||
|
config_widget.save_settings()
|
@ -0,0 +1,474 @@
|
|||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
from __future__ import (unicode_literals, division, absolute_import,
|
||||||
|
print_function)
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
|
||||||
|
import os, zipfile
|
||||||
|
|
||||||
|
try:
|
||||||
|
from PyQt5.Qt import QToolButton, QUrl
|
||||||
|
except ImportError:
|
||||||
|
from PyQt4.Qt import QToolButton, QUrl
|
||||||
|
|
||||||
|
from calibre.gui2 import open_url, question_dialog
|
||||||
|
from calibre.gui2.actions import InterfaceAction
|
||||||
|
from calibre.utils.config import config_dir
|
||||||
|
from calibre.ptempfile import (PersistentTemporaryDirectory,
|
||||||
|
PersistentTemporaryFile, remove_dir)
|
||||||
|
|
||||||
|
from calibre.ebooks.metadata.meta import get_metadata
|
||||||
|
|
||||||
|
from calibre_plugins.obok_dedrm.dialogs import (SelectionDialog, DecryptAddProgressDialog,
|
||||||
|
AddEpubFormatsProgressDialog, ResultsSummaryDialog)
|
||||||
|
from calibre_plugins.obok_dedrm.config import plugin_prefs as cfg
|
||||||
|
from calibre_plugins.obok_dedrm.__init__ import (PLUGIN_NAME, PLUGIN_SAFE_NAME,
|
||||||
|
PLUGIN_VERSION, PLUGIN_DESCRIPTION, HELPFILE_NAME)
|
||||||
|
from calibre_plugins.obok_dedrm.utilities import (
|
||||||
|
get_icon, set_plugin_icon_resources, showErrorDlg, format_plural,
|
||||||
|
debug_print
|
||||||
|
)
|
||||||
|
|
||||||
|
from calibre_plugins.obok_dedrm.obok.obok import KoboLibrary
|
||||||
|
from calibre_plugins.obok_dedrm.obok.legacy_obok import legacy_obok
|
||||||
|
|
||||||
|
PLUGIN_ICONS = ['images/obok.png']
|
||||||
|
|
||||||
|
try:
|
||||||
|
debug_print("obok::action_err.py - loading translations")
|
||||||
|
load_translations()
|
||||||
|
except NameError:
|
||||||
|
debug_print("obok::action_err.py - exception when loading translations")
|
||||||
|
pass # load_translations() added in calibre 1.9
|
||||||
|
|
||||||
|
class InterfacePluginAction(InterfaceAction):
|
||||||
|
name = PLUGIN_NAME
|
||||||
|
action_spec = (PLUGIN_NAME, None,
|
||||||
|
_(PLUGIN_DESCRIPTION), None)
|
||||||
|
popup_type = QToolButton.InstantPopup
|
||||||
|
action_type = 'current'
|
||||||
|
|
||||||
|
def genesis(self):
|
||||||
|
icon_resources = self.load_resources(PLUGIN_ICONS)
|
||||||
|
set_plugin_icon_resources(PLUGIN_NAME, icon_resources)
|
||||||
|
|
||||||
|
self.qaction.setIcon(get_icon(PLUGIN_ICONS[0]))
|
||||||
|
self.qaction.triggered.connect(self.launchObok)
|
||||||
|
self.gui.keyboard.finalize()
|
||||||
|
|
||||||
|
def launchObok(self):
|
||||||
|
'''
|
||||||
|
Main processing/distribution method
|
||||||
|
'''
|
||||||
|
self.count = 0
|
||||||
|
self.books_to_add = []
|
||||||
|
self.formats_to_add = []
|
||||||
|
self.add_books_cancelled = False
|
||||||
|
self.decryption_errors = []
|
||||||
|
self.userkeys = []
|
||||||
|
self.duplicate_book_list = []
|
||||||
|
self.no_home_for_book = []
|
||||||
|
self.ids_of_new_books = []
|
||||||
|
self.successful_format_adds =[]
|
||||||
|
self.add_formats_cancelled = False
|
||||||
|
self.tdir = PersistentTemporaryDirectory('_obok', prefix='')
|
||||||
|
self.db = self.gui.current_db.new_api
|
||||||
|
self.current_idx = self.gui.library_view.currentIndex()
|
||||||
|
|
||||||
|
print ('Running {}'.format(PLUGIN_NAME + ' v' + PLUGIN_VERSION))
|
||||||
|
# Get the Kobo Library object (obok v3.01)
|
||||||
|
self.library = KoboLibrary()
|
||||||
|
|
||||||
|
# Get a list of Kobo titles
|
||||||
|
books = self.build_book_list()
|
||||||
|
if len(books) < 1:
|
||||||
|
msg = _('<p>No books found in Kobo Library\nAre you sure it\'s installed\configured\synchronized?')
|
||||||
|
showErrorDlg(msg, None)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check to see if a key can be retrieved using the legacy obok method.
|
||||||
|
legacy_key = legacy_obok().get_legacy_cookie_id
|
||||||
|
if legacy_key is not None:
|
||||||
|
print (_('Legacy key found: '), legacy_key.encode('hex_codec'))
|
||||||
|
self.userkeys.append(legacy_key)
|
||||||
|
# Add userkeys found through the normal obok method to the list to try.
|
||||||
|
try:
|
||||||
|
candidate_keys = self.library.userkeys
|
||||||
|
except:
|
||||||
|
print (_('Trouble retrieving keys with newer obok method.'))
|
||||||
|
else:
|
||||||
|
if len(candidate_keys):
|
||||||
|
self.userkeys.extend(candidate_keys)
|
||||||
|
print (_('Found {0} possible keys to try.').format(len(self.userkeys)))
|
||||||
|
if not len(self.userkeys):
|
||||||
|
msg = _('<p>No userkeys found to decrypt books with. No point in proceeding.')
|
||||||
|
showErrorDlg(msg, None)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Launch the Dialog so the user can select titles.
|
||||||
|
dlg = SelectionDialog(self.gui, self, books)
|
||||||
|
if dlg.exec_():
|
||||||
|
books_to_import = dlg.getBooks()
|
||||||
|
self.count = len(books_to_import)
|
||||||
|
debug_print("InterfacePluginAction::launchObok - number of books to decrypt: %d" % self.count)
|
||||||
|
# Feed the titles, the callback function (self.get_decrypted_kobo_books)
|
||||||
|
# and the Kobo library object to the ProgressDialog dispatcher.
|
||||||
|
d = DecryptAddProgressDialog(self.gui, books_to_import, self.get_decrypted_kobo_books, self.library, 'kobo',
|
||||||
|
status_msg_type='Kobo books', action_type=('Decrypting', 'Decryption'))
|
||||||
|
# Canceled the decryption process; clean up and exit.
|
||||||
|
if d.wasCanceled():
|
||||||
|
print (_('{} - Decryption canceled by user.').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION))
|
||||||
|
self.library.close()
|
||||||
|
remove_dir(self.tdir)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
# Canceled the selection process; clean up and exit.
|
||||||
|
self.library.close()
|
||||||
|
remove_dir(self.tdir)
|
||||||
|
return
|
||||||
|
# Close Kobo Library object
|
||||||
|
self.library.close()
|
||||||
|
|
||||||
|
# If we have decrypted books to work with, feed the list of decrypted books details
|
||||||
|
# and the callback function (self.add_new_books) to the ProgressDialog dispatcher.
|
||||||
|
if len(self.books_to_add):
|
||||||
|
d = DecryptAddProgressDialog(self.gui, self.books_to_add, self.add_new_books, self.db, 'calibre',
|
||||||
|
status_msg_type='new calibre books', action_type=('Adding','Addition'))
|
||||||
|
# Canceled the "add new books to calibre" process;
|
||||||
|
# show the results of what got added before cancellation.
|
||||||
|
if d.wasCanceled():
|
||||||
|
print (_('{} - "Add books" canceled by user.').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION))
|
||||||
|
self.add_books_cancelled = True
|
||||||
|
print (_('{} - wrapping up results.').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION))
|
||||||
|
self.wrap_up_results()
|
||||||
|
remove_dir(self.tdir)
|
||||||
|
return
|
||||||
|
# If books couldn't be added because of duplicate entries in calibre, ask
|
||||||
|
# if we should try to add the decrypted epubs to existing calibre library entries.
|
||||||
|
if len(self.duplicate_book_list):
|
||||||
|
if cfg['finding_homes_for_formats'] == 'Always':
|
||||||
|
self.process_epub_formats()
|
||||||
|
elif cfg['finding_homes_for_formats'] == 'Never':
|
||||||
|
self.no_home_for_book.extend([entry[0] for entry in self.duplicate_book_list])
|
||||||
|
else:
|
||||||
|
if self.ask_about_inserting_epubs():
|
||||||
|
# Find homes for the epub decrypted formats in existing calibre library entries.
|
||||||
|
self.process_epub_formats()
|
||||||
|
else:
|
||||||
|
print (_('{} - User opted not to try to insert EPUB formats').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION))
|
||||||
|
self.no_home_for_book.extend([entry[0] for entry in self.duplicate_book_list])
|
||||||
|
|
||||||
|
print (_('{} - wrapping up results.').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION))
|
||||||
|
self.wrap_up_results()
|
||||||
|
remove_dir(self.tdir)
|
||||||
|
return
|
||||||
|
|
||||||
|
def show_help(self):
|
||||||
|
'''
|
||||||
|
Extract on demand the help file resource
|
||||||
|
'''
|
||||||
|
def get_help_file_resource():
|
||||||
|
# We will write the help file out every time, in case the user upgrades the plugin zip
|
||||||
|
# and there is a newer help file contained within it.
|
||||||
|
file_path = os.path.join(config_dir, 'plugins', HELPFILE_NAME)
|
||||||
|
file_data = self.load_resources(HELPFILE_NAME)[HELPFILE_NAME]
|
||||||
|
with open(file_path,'w') as f:
|
||||||
|
f.write(file_data)
|
||||||
|
return file_path
|
||||||
|
url = 'file:///' + get_help_file_resource()
|
||||||
|
open_url(QUrl(url))
|
||||||
|
|
||||||
|
def build_book_list(self):
|
||||||
|
'''
|
||||||
|
Connect to Kobo db and get titles.
|
||||||
|
'''
|
||||||
|
return self.library.books
|
||||||
|
|
||||||
|
def get_decrypted_kobo_books(self, book):
|
||||||
|
'''
|
||||||
|
This method is a call-back function used by DecryptAddProgressDialog in dialogs.py to decrypt Kobo books
|
||||||
|
|
||||||
|
:param book: A KoboBook object that is to be decrypted.
|
||||||
|
'''
|
||||||
|
print (_('{0} - Decrypting {1}').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION, book.title))
|
||||||
|
decrypted = self.decryptBook(book)
|
||||||
|
if decrypted['success']:
|
||||||
|
# Build a list of calibre "book maps" for calibre's add_book function.
|
||||||
|
mi = get_metadata(decrypted['fileobj'], 'epub')
|
||||||
|
bookmap = {'EPUB':decrypted['fileobj'].name}
|
||||||
|
self.books_to_add.append((mi, bookmap))
|
||||||
|
else:
|
||||||
|
# Book is probably still encrypted.
|
||||||
|
print (_('{0} - Couldn\'t decrypt {1}').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION, book.title))
|
||||||
|
self.decryption_errors.append((book.title, _('decryption errors')))
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def add_new_books(self, books_to_add):
|
||||||
|
'''
|
||||||
|
This method is a call-back function used by DecryptAddProgressDialog in dialogs.py to add books to calibre
|
||||||
|
(It's set up to handle multiple books, but will only be fed books one at a time by DecryptAddProgressDialog)
|
||||||
|
|
||||||
|
:param books_to_add: List of calibre bookmaps (created in get_decrypted_kobo_books)
|
||||||
|
'''
|
||||||
|
added = self.db.add_books(books_to_add, add_duplicates=False, run_hooks=False)
|
||||||
|
if len(added[0]):
|
||||||
|
# Record the id(s) that got added
|
||||||
|
for id in added[0]:
|
||||||
|
print (_('{0} - Added {1}').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION, books_to_add[0][0].title))
|
||||||
|
self.ids_of_new_books.append((id, books_to_add[0][0]))
|
||||||
|
if len(added[1]):
|
||||||
|
# Build a list of details about the books that didn't get added because duplicate were detected.
|
||||||
|
for mi, map in added[1]:
|
||||||
|
print (_('{0} - {1} already exists. Will try to add format later.').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION, mi.title))
|
||||||
|
self.duplicate_book_list.append((mi, map['EPUB'], _('duplicate detected')))
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def add_epub_format(self, book_id, mi, path):
|
||||||
|
'''
|
||||||
|
This method is a call-back function used by AddEpubFormatsProgressDialog in dialogs.py
|
||||||
|
|
||||||
|
:param book_id: calibre ID of the book to add the encrypted epub to.
|
||||||
|
:param mi: calibre metadata object
|
||||||
|
:param path: path to the decrypted epub (temp file)
|
||||||
|
'''
|
||||||
|
if self.db.add_format(book_id, 'EPUB', path, replace=False, run_hooks=False):
|
||||||
|
self.successful_format_adds.append((book_id, mi))
|
||||||
|
print (_('{0} - Successfully added EPUB format to existing {1}').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION, mi.title))
|
||||||
|
return True
|
||||||
|
# we really shouldn't get here.
|
||||||
|
print (_('{0} - Error adding EPUB format to existing {1}. This really shouldn\'t happen.').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION, mi.title))
|
||||||
|
self.no_home_for_book.append(mi)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def process_epub_formats(self):
|
||||||
|
'''
|
||||||
|
Ask the user if they want to try to find homes for those books that already had an entry in calibre
|
||||||
|
'''
|
||||||
|
for book in self.duplicate_book_list:
|
||||||
|
mi, tmp_file = book[0], book[1]
|
||||||
|
dup_ids = self.db.find_identical_books(mi)
|
||||||
|
home_id = self.find_a_home(dup_ids)
|
||||||
|
if home_id is not None:
|
||||||
|
# Found an epub-free duplicate to add the epub to.
|
||||||
|
# build a list for the add_epub_format method to use.
|
||||||
|
self.formats_to_add.append((home_id, mi, tmp_file))
|
||||||
|
else:
|
||||||
|
self.no_home_for_book.append(mi)
|
||||||
|
# If we found homes for decrypted epubs in existing calibre entries, feed the list of decrypted book
|
||||||
|
# details and the callback function (self.add_epub_format) to the ProgressDialog dispatcher.
|
||||||
|
if self.formats_to_add:
|
||||||
|
d = AddEpubFormatsProgressDialog(self.gui, self.formats_to_add, self.add_epub_format)
|
||||||
|
if d.wasCanceled():
|
||||||
|
print (_('{} - "Insert formats" canceled by user.').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION))
|
||||||
|
self.add_formats_cancelled = True
|
||||||
|
return
|
||||||
|
#return
|
||||||
|
return
|
||||||
|
|
||||||
|
def wrap_up_results(self):
|
||||||
|
'''
|
||||||
|
Present the results
|
||||||
|
'''
|
||||||
|
caption = PLUGIN_NAME + ' v' + PLUGIN_VERSION
|
||||||
|
# Refresh the gui and highlight new entries/modified entries.
|
||||||
|
if len(self.ids_of_new_books) or len(self.successful_format_adds):
|
||||||
|
self.refresh_gui_lib()
|
||||||
|
|
||||||
|
msg, log = self.build_report()
|
||||||
|
|
||||||
|
sd = ResultsSummaryDialog(self.gui, caption, msg, log)
|
||||||
|
sd.exec_()
|
||||||
|
return
|
||||||
|
|
||||||
|
def ask_about_inserting_epubs(self):
|
||||||
|
'''
|
||||||
|
Build question dialog with details about kobo books
|
||||||
|
that couldn't be added to calibre as new books.
|
||||||
|
'''
|
||||||
|
''' Terisa: Improve the message
|
||||||
|
'''
|
||||||
|
caption = PLUGIN_NAME + ' v' + PLUGIN_VERSION
|
||||||
|
plural = format_plural(len(self.ids_of_new_books))
|
||||||
|
det_msg = ''
|
||||||
|
if self.count > 1:
|
||||||
|
msg = _('<p><b>{0}</b> EPUB{2} successfully added to library.<br /><br /><b>{1}</b> ').format(len(self.ids_of_new_books), len(self.duplicate_book_list), plural)
|
||||||
|
msg += _('not added because books with the same title/author were detected.<br /><br />Would you like to try and add the EPUB format{0}').format(plural)
|
||||||
|
msg += _(' to those existing entries?<br /><br />NOTE: no pre-existing EPUBs will be overwritten.')
|
||||||
|
for entry in self.duplicate_book_list:
|
||||||
|
det_msg += _('{0} -- not added because of {1} in your library.\n\n').format(entry[0].title, entry[2])
|
||||||
|
else:
|
||||||
|
msg = _('<p><b>{0}</b> -- not added because of {1} in your library.<br /><br />').format(self.duplicate_book_list[0][0].title, self.duplicate_book_list[0][2])
|
||||||
|
msg += _('Would you like to try and add the EPUB format to an available calibre duplicate?<br /><br />')
|
||||||
|
msg += _('NOTE: no pre-existing EPUB will be overwritten.')
|
||||||
|
|
||||||
|
return question_dialog(self.gui, caption, msg, det_msg)
|
||||||
|
|
||||||
|
def find_a_home(self, ids):
|
||||||
|
'''
|
||||||
|
Find the ID of the first EPUB-Free duplicate available
|
||||||
|
|
||||||
|
:param ids: List of calibre IDs that might serve as a home.
|
||||||
|
'''
|
||||||
|
for id in ids:
|
||||||
|
# Find the first entry that matches the incoming book that doesn't have an EPUB format.
|
||||||
|
if not self.db.has_format(id, 'EPUB'):
|
||||||
|
return id
|
||||||
|
break
|
||||||
|
return None
|
||||||
|
|
||||||
|
def refresh_gui_lib(self):
|
||||||
|
'''
|
||||||
|
Update the GUI; highlight the books that were added/modified
|
||||||
|
'''
|
||||||
|
if self.current_idx.isValid():
|
||||||
|
self.gui.library_view.model().current_changed(self.current_idx, self.current_idx)
|
||||||
|
new_entries = [id for id, mi in self.ids_of_new_books]
|
||||||
|
if new_entries:
|
||||||
|
self.gui.library_view.model().db.data.books_added(new_entries)
|
||||||
|
self.gui.library_view.model().books_added(len(new_entries))
|
||||||
|
new_entries.extend([id for id, mi in self.successful_format_adds])
|
||||||
|
self.gui.db_images.reset()
|
||||||
|
self.gui.tags_view.recount()
|
||||||
|
self.gui.library_view.model().set_highlight_only(True)
|
||||||
|
self.gui.library_view.select_rows(new_entries)
|
||||||
|
return
|
||||||
|
|
||||||
|
def decryptBook(self, book):
|
||||||
|
'''
|
||||||
|
Decrypt Kobo book
|
||||||
|
|
||||||
|
:param book: obok file object
|
||||||
|
'''
|
||||||
|
result = {}
|
||||||
|
result['success'] = False
|
||||||
|
result['fileobj'] = None
|
||||||
|
|
||||||
|
zin = zipfile.ZipFile(book.filename, 'r')
|
||||||
|
#print ('Kobo library filename: {0}'.format(book.filename))
|
||||||
|
for userkey in self.userkeys:
|
||||||
|
print (_('Trying key: '), userkey.encode('hex_codec'))
|
||||||
|
check = True
|
||||||
|
try:
|
||||||
|
fileout = PersistentTemporaryFile('.epub', dir=self.tdir)
|
||||||
|
#print ('Temp file: {0}'.format(fileout.name))
|
||||||
|
# modify the output file to be compressed by default
|
||||||
|
zout = zipfile.ZipFile(fileout.name, "w", zipfile.ZIP_DEFLATED)
|
||||||
|
# ensure that the mimetype file is the first written to the epub container
|
||||||
|
# and is stored with no compression
|
||||||
|
members = zin.namelist();
|
||||||
|
try:
|
||||||
|
members.remove('mimetype')
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
zout.writestr('mimetype', 'application/epub+zip', zipfile.ZIP_STORED)
|
||||||
|
# end of mimetype mod
|
||||||
|
for filename in members:
|
||||||
|
contents = zin.read(filename)
|
||||||
|
if filename in book.encryptedfiles:
|
||||||
|
file = book.encryptedfiles[filename]
|
||||||
|
contents = file.decrypt(userkey, contents)
|
||||||
|
# Parse failures mean the key is probably wrong.
|
||||||
|
if check:
|
||||||
|
check = not file.check(contents)
|
||||||
|
zout.writestr(filename, contents)
|
||||||
|
zout.close()
|
||||||
|
zin.close()
|
||||||
|
result['success'] = True
|
||||||
|
result['fileobj'] = fileout
|
||||||
|
print ('Success!')
|
||||||
|
return result
|
||||||
|
except ValueError:
|
||||||
|
print (_('Decryption failed, trying next key.'))
|
||||||
|
zout.close()
|
||||||
|
continue
|
||||||
|
except Exception:
|
||||||
|
print (_('Unknown Error decrypting, trying next key..'))
|
||||||
|
zout.close()
|
||||||
|
continue
|
||||||
|
result['fileobj'] = book.filename
|
||||||
|
zin.close()
|
||||||
|
return result
|
||||||
|
|
||||||
|
def build_report(self):
|
||||||
|
log = ''
|
||||||
|
processed = len(self.ids_of_new_books) + len(self.successful_format_adds)
|
||||||
|
|
||||||
|
if processed == self.count:
|
||||||
|
if self.count > 1:
|
||||||
|
msg = _('<p>All selected Kobo books added as new calibre books or inserted into existing calibre ebooks.<br /><br />No issues.')
|
||||||
|
else:
|
||||||
|
# Single book ... don't get fancy.
|
||||||
|
title = self.ids_of_new_books[0][1].title if self.ids_of_new_books else self.successful_format_adds[0][1].title
|
||||||
|
msg = _('<p>{0} successfully added.').format(title)
|
||||||
|
return (msg, log)
|
||||||
|
else:
|
||||||
|
if self.count != 1:
|
||||||
|
msg = _('<p>Not all selected Kobo books made it into calibre.<br /><br />View report for details.')
|
||||||
|
log += _('<p><b>Total attempted:</b> {}</p>\n').format(self.count)
|
||||||
|
log += _('<p><b>Decryption errors:</b> {}</p>\n').format(len(self.decryption_errors))
|
||||||
|
if self.decryption_errors:
|
||||||
|
log += '<ul>\n'
|
||||||
|
for title, reason in self.decryption_errors:
|
||||||
|
log += '<li>{}</li>\n'.format(title)
|
||||||
|
log += '</ul>\n'
|
||||||
|
log += _('<p><b>New Books created:</b> {}</p>\n').format(len(self.ids_of_new_books))
|
||||||
|
if self.ids_of_new_books:
|
||||||
|
log += '<ul>\n'
|
||||||
|
for id, mi in self.ids_of_new_books:
|
||||||
|
log += '<li>{}</li>\n'.format(mi.title)
|
||||||
|
log += '</ul>\n'
|
||||||
|
if self.add_books_cancelled:
|
||||||
|
log += _('<p><b>Duplicates that weren\'t added:</b> {}</p>\n').format(len(self.duplicate_book_list))
|
||||||
|
if self.duplicate_book_list:
|
||||||
|
log += '<ul>\n'
|
||||||
|
for book in self.duplicate_book_list:
|
||||||
|
log += '<li>{}</li>\n'.format(book[0].title)
|
||||||
|
log += '</ul>\n'
|
||||||
|
cancelled_count = self.count - (len(self.decryption_errors) + len(self.ids_of_new_books) + len(self.duplicate_book_list))
|
||||||
|
if cancelled_count > 0:
|
||||||
|
log += _('<p><b>Book imports cancelled by user:</b> {}</p>\n').format(cancelled_count)
|
||||||
|
return (msg, log)
|
||||||
|
log += _('<p><b>New EPUB formats inserted in existing calibre books:</b> {0}</p>\n').format(len(self.successful_format_adds))
|
||||||
|
if self.successful_format_adds:
|
||||||
|
log += '<ul>\n'
|
||||||
|
for id, mi in self.successful_format_adds:
|
||||||
|
log += '<li>{}</li>\n'.format(mi.title)
|
||||||
|
log += '</ul>\n'
|
||||||
|
log += _('<p><b>EPUB formats NOT inserted into existing calibre books:</b> {}<br />\n').format(len(self.no_home_for_book))
|
||||||
|
log += _('(Either because the user <i>chose</i> not to insert them, or because all duplicates already had an EPUB format)')
|
||||||
|
if self.no_home_for_book:
|
||||||
|
log += '<ul>\n'
|
||||||
|
for mi in self.no_home_for_book:
|
||||||
|
log += '<li>{}</li>\n'.format(mi.title)
|
||||||
|
log += '</ul>\n'
|
||||||
|
if self.add_formats_cancelled:
|
||||||
|
cancelled_count = self.count - (len(self.decryption_errors) + len(self.ids_of_new_books) + len(self.successful_format_adds) + len(self.no_home_for_book))
|
||||||
|
if cancelled_count > 0:
|
||||||
|
log += _('<p><b>Format imports cancelled by user:</b> {}</p>\n').format(cancelled_count)
|
||||||
|
return (msg, log)
|
||||||
|
else:
|
||||||
|
|
||||||
|
# Single book ... don't get fancy.
|
||||||
|
if self.ids_of_new_books:
|
||||||
|
title = self.ids_of_new_books[0][1].title
|
||||||
|
elif self.successful_format_adds:
|
||||||
|
title = self.successful_format_adds[0][1].title
|
||||||
|
elif self.no_home_for_book:
|
||||||
|
title = self.no_home_for_book[0].title
|
||||||
|
elif self.decryption_errors:
|
||||||
|
title = self.decryption_errors[0][0]
|
||||||
|
else:
|
||||||
|
title = _('Unknown Book Title')
|
||||||
|
if self.decryption_errors:
|
||||||
|
reason = _('it couldn\'t be decrypted.')
|
||||||
|
elif self.no_home_for_book:
|
||||||
|
reason = _('user CHOSE not to insert the new EPUB format, or all existing calibre entries HAD an EPUB format already.')
|
||||||
|
else:
|
||||||
|
reason = _('of unknown reasons. Gosh I\'m embarrassed!')
|
||||||
|
msg = _('<p>{0} not added because {1}').format(title, reason)
|
||||||
|
return (msg, log)
|
||||||
|
|
@ -0,0 +1,589 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
from __future__ import (unicode_literals, division, absolute_import,
|
||||||
|
print_function)
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2012, David Forrester <davidfor@internode.on.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import os, time, re, sys
|
||||||
|
try:
|
||||||
|
from PyQt5.Qt import (Qt, QIcon, QPixmap, QLabel, QDialog, QHBoxLayout, QProgressBar,
|
||||||
|
QTableWidgetItem, QFont, QLineEdit, QComboBox,
|
||||||
|
QVBoxLayout, QDialogButtonBox, QStyledItemDelegate, QDateTime,
|
||||||
|
QRegExpValidator, QRegExp, QDate, QDateEdit)
|
||||||
|
except ImportError:
|
||||||
|
from PyQt4.Qt import (Qt, QIcon, QPixmap, QLabel, QDialog, QHBoxLayout, QProgressBar,
|
||||||
|
QTableWidgetItem, QFont, QLineEdit, QComboBox,
|
||||||
|
QVBoxLayout, QDialogButtonBox, QStyledItemDelegate, QDateTime,
|
||||||
|
QRegExpValidator, QRegExp, QDate, QDateEdit)
|
||||||
|
|
||||||
|
from calibre.constants import iswindows, filesystem_encoding, DEBUG
|
||||||
|
from calibre.gui2 import gprefs, error_dialog, UNDEFINED_QDATETIME, Application
|
||||||
|
from calibre.gui2.actions import menu_action_unique_name
|
||||||
|
from calibre.gui2.keyboard import ShortcutConfig
|
||||||
|
from calibre.utils.config import config_dir, tweaks
|
||||||
|
from calibre.utils.date import now, format_date, qt_to_dt, UNDEFINED_DATE, as_local_time
|
||||||
|
from calibre import prints
|
||||||
|
|
||||||
|
# Global definition of our plugin name. Used for common functions that require this.
|
||||||
|
plugin_name = None
|
||||||
|
# Global definition of our plugin resources. Used to share between the xxxAction and xxxBase
|
||||||
|
# classes if you need any zip images to be displayed on the configuration dialog.
|
||||||
|
plugin_icon_resources = {}
|
||||||
|
|
||||||
|
BASE_TIME = None
|
||||||
|
def debug_print(*args):
|
||||||
|
global BASE_TIME
|
||||||
|
if BASE_TIME is None:
|
||||||
|
BASE_TIME = time.time()
|
||||||
|
if DEBUG:
|
||||||
|
prints('DEBUG: %6.1f'%(time.time()-BASE_TIME), *args)
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
debug_print("obok::common_utils.py - loading translations")
|
||||||
|
load_translations()
|
||||||
|
except NameError:
|
||||||
|
debug_print("obok::common_utils.py - exception when loading translations")
|
||||||
|
pass # load_translations() added in calibre 1.9
|
||||||
|
|
||||||
|
def set_plugin_icon_resources(name, resources):
|
||||||
|
'''
|
||||||
|
Set our global store of plugin name and icon resources for sharing between
|
||||||
|
the InterfaceAction class which reads them and the ConfigWidget
|
||||||
|
if needed for use on the customization dialog for this plugin.
|
||||||
|
'''
|
||||||
|
global plugin_icon_resources, plugin_name
|
||||||
|
plugin_name = name
|
||||||
|
plugin_icon_resources = resources
|
||||||
|
|
||||||
|
def get_icon(icon_name):
|
||||||
|
'''
|
||||||
|
Retrieve a QIcon for the named image from the zip file if it exists,
|
||||||
|
or if not then from Calibre's image cache.
|
||||||
|
'''
|
||||||
|
if icon_name:
|
||||||
|
pixmap = get_pixmap(icon_name)
|
||||||
|
if pixmap is None:
|
||||||
|
# Look in Calibre's cache for the icon
|
||||||
|
return QIcon(I(icon_name))
|
||||||
|
else:
|
||||||
|
return QIcon(pixmap)
|
||||||
|
return QIcon()
|
||||||
|
|
||||||
|
|
||||||
|
def get_pixmap(icon_name):
|
||||||
|
'''
|
||||||
|
Retrieve a QPixmap for the named image
|
||||||
|
Any icons belonging to the plugin must be prefixed with 'images/'
|
||||||
|
'''
|
||||||
|
global plugin_icon_resources, plugin_name
|
||||||
|
|
||||||
|
if not icon_name.startswith('images/'):
|
||||||
|
# We know this is definitely not an icon belonging to this plugin
|
||||||
|
pixmap = QPixmap()
|
||||||
|
pixmap.load(I(icon_name))
|
||||||
|
return pixmap
|
||||||
|
|
||||||
|
# Check to see whether the icon exists as a Calibre resource
|
||||||
|
# This will enable skinning if the user stores icons within a folder like:
|
||||||
|
# ...\AppData\Roaming\calibre\resources\images\Plugin Name\
|
||||||
|
if plugin_name:
|
||||||
|
local_images_dir = get_local_images_dir(plugin_name)
|
||||||
|
local_image_path = os.path.join(local_images_dir, icon_name.replace('images/', ''))
|
||||||
|
if os.path.exists(local_image_path):
|
||||||
|
pixmap = QPixmap()
|
||||||
|
pixmap.load(local_image_path)
|
||||||
|
return pixmap
|
||||||
|
|
||||||
|
# As we did not find an icon elsewhere, look within our zip resources
|
||||||
|
if icon_name in plugin_icon_resources:
|
||||||
|
pixmap = QPixmap()
|
||||||
|
pixmap.loadFromData(plugin_icon_resources[icon_name])
|
||||||
|
return pixmap
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_local_images_dir(subfolder=None):
|
||||||
|
'''
|
||||||
|
Returns a path to the user's local resources/images folder
|
||||||
|
If a subfolder name parameter is specified, appends this to the path
|
||||||
|
'''
|
||||||
|
images_dir = os.path.join(config_dir, 'resources/images')
|
||||||
|
if subfolder:
|
||||||
|
images_dir = os.path.join(images_dir, subfolder)
|
||||||
|
if iswindows:
|
||||||
|
images_dir = os.path.normpath(images_dir)
|
||||||
|
return images_dir
|
||||||
|
|
||||||
|
|
||||||
|
def create_menu_item(ia, parent_menu, menu_text, image=None, tooltip=None,
|
||||||
|
shortcut=(), triggered=None, is_checked=None):
|
||||||
|
'''
|
||||||
|
Create a menu action with the specified criteria and action
|
||||||
|
Note that if no shortcut is specified, will not appear in Preferences->Keyboard
|
||||||
|
This method should only be used for actions which either have no shortcuts,
|
||||||
|
or register their menus only once. Use create_menu_action_unique for all else.
|
||||||
|
'''
|
||||||
|
if shortcut is not None:
|
||||||
|
if len(shortcut) == 0:
|
||||||
|
shortcut = ()
|
||||||
|
else:
|
||||||
|
shortcut = _(shortcut)
|
||||||
|
ac = ia.create_action(spec=(menu_text, None, tooltip, shortcut),
|
||||||
|
attr=menu_text)
|
||||||
|
if image:
|
||||||
|
ac.setIcon(get_icon(image))
|
||||||
|
if triggered is not None:
|
||||||
|
ac.triggered.connect(triggered)
|
||||||
|
if is_checked is not None:
|
||||||
|
ac.setCheckable(True)
|
||||||
|
if is_checked:
|
||||||
|
ac.setChecked(True)
|
||||||
|
|
||||||
|
parent_menu.addAction(ac)
|
||||||
|
return ac
|
||||||
|
|
||||||
|
|
||||||
|
def create_menu_action_unique(ia, parent_menu, menu_text, image=None, tooltip=None,
|
||||||
|
shortcut=None, triggered=None, is_checked=None, shortcut_name=None,
|
||||||
|
unique_name=None):
|
||||||
|
'''
|
||||||
|
Create a menu action with the specified criteria and action, using the new
|
||||||
|
InterfaceAction.create_menu_action() function which ensures that regardless of
|
||||||
|
whether a shortcut is specified it will appear in Preferences->Keyboard
|
||||||
|
'''
|
||||||
|
orig_shortcut = shortcut
|
||||||
|
kb = ia.gui.keyboard
|
||||||
|
if unique_name is None:
|
||||||
|
unique_name = menu_text
|
||||||
|
if not shortcut == False:
|
||||||
|
full_unique_name = menu_action_unique_name(ia, unique_name)
|
||||||
|
if full_unique_name in kb.shortcuts:
|
||||||
|
shortcut = False
|
||||||
|
else:
|
||||||
|
if shortcut is not None and not shortcut == False:
|
||||||
|
if len(shortcut) == 0:
|
||||||
|
shortcut = None
|
||||||
|
else:
|
||||||
|
shortcut = _(shortcut)
|
||||||
|
|
||||||
|
if shortcut_name is None:
|
||||||
|
shortcut_name = menu_text.replace('&','')
|
||||||
|
|
||||||
|
ac = ia.create_menu_action(parent_menu, unique_name, menu_text, icon=None, shortcut=shortcut,
|
||||||
|
description=tooltip, triggered=triggered, shortcut_name=shortcut_name)
|
||||||
|
if shortcut == False and not orig_shortcut == False:
|
||||||
|
if ac.calibre_shortcut_unique_name in ia.gui.keyboard.shortcuts:
|
||||||
|
kb.replace_action(ac.calibre_shortcut_unique_name, ac)
|
||||||
|
if image:
|
||||||
|
ac.setIcon(get_icon(image))
|
||||||
|
if is_checked is not None:
|
||||||
|
ac.setCheckable(True)
|
||||||
|
if is_checked:
|
||||||
|
ac.setChecked(True)
|
||||||
|
return ac
|
||||||
|
|
||||||
|
|
||||||
|
def get_library_uuid(db):
|
||||||
|
try:
|
||||||
|
library_uuid = db.library_id
|
||||||
|
except:
|
||||||
|
library_uuid = ''
|
||||||
|
return library_uuid
|
||||||
|
|
||||||
|
|
||||||
|
class ImageLabel(QLabel):
|
||||||
|
|
||||||
|
def __init__(self, parent, icon_name, size=16):
|
||||||
|
QLabel.__init__(self, parent)
|
||||||
|
pixmap = get_pixmap(icon_name)
|
||||||
|
self.setPixmap(pixmap)
|
||||||
|
self.setMaximumSize(size, size)
|
||||||
|
self.setScaledContents(True)
|
||||||
|
|
||||||
|
|
||||||
|
class ImageTitleLayout(QHBoxLayout):
|
||||||
|
'''
|
||||||
|
A reusable layout widget displaying an image followed by a title
|
||||||
|
'''
|
||||||
|
def __init__(self, parent, icon_name, title):
|
||||||
|
QHBoxLayout.__init__(self)
|
||||||
|
self.title_image_label = QLabel(parent)
|
||||||
|
self.update_title_icon(icon_name)
|
||||||
|
self.addWidget(self.title_image_label)
|
||||||
|
|
||||||
|
title_font = QFont()
|
||||||
|
title_font.setPointSize(16)
|
||||||
|
shelf_label = QLabel(title, parent)
|
||||||
|
shelf_label.setFont(title_font)
|
||||||
|
self.addWidget(shelf_label)
|
||||||
|
self.insertStretch(-1)
|
||||||
|
|
||||||
|
# Add hyperlink to a help file at the right. We will replace the correct name when it is clicked.
|
||||||
|
help_label = QLabel(('<a href="http://www.foo.com/">{0}</a>').format(_("Help")), parent)
|
||||||
|
help_label.setTextInteractionFlags(Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard)
|
||||||
|
help_label.setAlignment(Qt.AlignRight)
|
||||||
|
help_label.linkActivated.connect(parent.help_link_activated)
|
||||||
|
self.addWidget(help_label)
|
||||||
|
|
||||||
|
def update_title_icon(self, icon_name):
|
||||||
|
pixmap = get_pixmap(icon_name)
|
||||||
|
if pixmap is None:
|
||||||
|
error_dialog(self.parent(), _("Restart required"),
|
||||||
|
_("Title image not found - you must restart Calibre before using this plugin!"), show=True)
|
||||||
|
else:
|
||||||
|
self.title_image_label.setPixmap(pixmap)
|
||||||
|
self.title_image_label.setMaximumSize(32, 32)
|
||||||
|
self.title_image_label.setScaledContents(True)
|
||||||
|
|
||||||
|
|
||||||
|
class SizePersistedDialog(QDialog):
|
||||||
|
'''
|
||||||
|
This dialog is a base class for any dialogs that want their size/position
|
||||||
|
restored when they are next opened.
|
||||||
|
'''
|
||||||
|
def __init__(self, parent, unique_pref_name):
|
||||||
|
QDialog.__init__(self, parent)
|
||||||
|
self.unique_pref_name = unique_pref_name
|
||||||
|
self.geom = gprefs.get(unique_pref_name, None)
|
||||||
|
self.finished.connect(self.dialog_closing)
|
||||||
|
self.help_anchor = ''
|
||||||
|
|
||||||
|
def resize_dialog(self):
|
||||||
|
if self.geom is None:
|
||||||
|
self.resize(self.sizeHint())
|
||||||
|
else:
|
||||||
|
self.restoreGeometry(self.geom)
|
||||||
|
|
||||||
|
def dialog_closing(self, result):
|
||||||
|
geom = bytearray(self.saveGeometry())
|
||||||
|
gprefs[self.unique_pref_name] = geom
|
||||||
|
|
||||||
|
def help_link_activated(self, url):
|
||||||
|
self.plugin_action.show_help(anchor=self.help_anchor)
|
||||||
|
|
||||||
|
|
||||||
|
class ReadOnlyTableWidgetItem(QTableWidgetItem):
|
||||||
|
|
||||||
|
def __init__(self, text):
|
||||||
|
if text is None:
|
||||||
|
text = ''
|
||||||
|
QTableWidgetItem.__init__(self, text, QTableWidgetItem.UserType)
|
||||||
|
self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
|
||||||
|
|
||||||
|
class RatingTableWidgetItem(QTableWidgetItem):
|
||||||
|
|
||||||
|
def __init__(self, rating, is_read_only=False):
|
||||||
|
QTableWidgetItem.__init__(self, '', QTableWidgetItem.UserType)
|
||||||
|
self.setData(Qt.DisplayRole, rating)
|
||||||
|
if is_read_only:
|
||||||
|
self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
|
||||||
|
|
||||||
|
|
||||||
|
class DateTableWidgetItem(QTableWidgetItem):
|
||||||
|
|
||||||
|
def __init__(self, date_read, is_read_only=False, default_to_today=False, fmt=None):
|
||||||
|
# debug_print("DateTableWidgetItem:__init__ - date_read=", date_read)
|
||||||
|
if date_read is None or date_read == UNDEFINED_DATE and default_to_today:
|
||||||
|
date_read = now()
|
||||||
|
if is_read_only:
|
||||||
|
QTableWidgetItem.__init__(self, format_date(date_read, fmt), QTableWidgetItem.UserType)
|
||||||
|
self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
|
||||||
|
self.setData(Qt.DisplayRole, QDateTime(date_read))
|
||||||
|
else:
|
||||||
|
QTableWidgetItem.__init__(self, '', QTableWidgetItem.UserType)
|
||||||
|
self.setData(Qt.DisplayRole, QDateTime(date_read))
|
||||||
|
|
||||||
|
from calibre.gui2.library.delegates import DateDelegate as _DateDelegate
|
||||||
|
class DateDelegate(_DateDelegate):
|
||||||
|
'''
|
||||||
|
Delegate for dates. Because this delegate stores the
|
||||||
|
format as an instance variable, a new instance must be created for each
|
||||||
|
column. This differs from all the other delegates.
|
||||||
|
'''
|
||||||
|
def __init__(self, parent, fmt='dd MMM yyyy', default_to_today=True):
|
||||||
|
_DateDelegate.__init__(self, parent)
|
||||||
|
self.format = fmt
|
||||||
|
self.default_to_today = default_to_today
|
||||||
|
|
||||||
|
# def displayText(self, val, locale):
|
||||||
|
# d = val.toDateTime()
|
||||||
|
# if d <= UNDEFINED_QDATETIME:
|
||||||
|
# return ''
|
||||||
|
# return format_date(qt_to_dt(d, as_utc=False), self.format)
|
||||||
|
|
||||||
|
def createEditor(self, parent, option, index):
|
||||||
|
qde = QStyledItemDelegate.createEditor(self, parent, option, index)
|
||||||
|
qde.setDisplayFormat(self.format)
|
||||||
|
qde.setMinimumDateTime(UNDEFINED_QDATETIME)
|
||||||
|
qde.setSpecialValueText(_('Undefined'))
|
||||||
|
qde.setCalendarPopup(True)
|
||||||
|
return qde
|
||||||
|
|
||||||
|
def setEditorData(self, editor, index):
|
||||||
|
val = index.model().data(index, Qt.DisplayRole).toDateTime()
|
||||||
|
if val is None or val == UNDEFINED_QDATETIME:
|
||||||
|
if self.default_to_today:
|
||||||
|
val = self.default_date
|
||||||
|
else:
|
||||||
|
val = UNDEFINED_QDATETIME
|
||||||
|
editor.setDateTime(val)
|
||||||
|
|
||||||
|
def setModelData(self, editor, model, index):
|
||||||
|
val = editor.dateTime()
|
||||||
|
if val <= UNDEFINED_QDATETIME:
|
||||||
|
model.setData(index, UNDEFINED_QDATETIME, Qt.EditRole)
|
||||||
|
else:
|
||||||
|
model.setData(index, QDateTime(val), Qt.EditRole)
|
||||||
|
|
||||||
|
|
||||||
|
class NoWheelComboBox(QComboBox):
|
||||||
|
|
||||||
|
def wheelEvent (self, event):
|
||||||
|
# Disable the mouse wheel on top of the combo box changing selection as plays havoc in a grid
|
||||||
|
event.ignore()
|
||||||
|
|
||||||
|
|
||||||
|
class CheckableTableWidgetItem(QTableWidgetItem):
|
||||||
|
|
||||||
|
def __init__(self, checked=False, is_tristate=False):
|
||||||
|
QTableWidgetItem.__init__(self, '')
|
||||||
|
self.setFlags(Qt.ItemFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled ))
|
||||||
|
if is_tristate:
|
||||||
|
self.setFlags(self.flags() | Qt.ItemIsTristate)
|
||||||
|
if checked:
|
||||||
|
self.setCheckState(Qt.Checked)
|
||||||
|
else:
|
||||||
|
if is_tristate and checked is None:
|
||||||
|
self.setCheckState(Qt.PartiallyChecked)
|
||||||
|
else:
|
||||||
|
self.setCheckState(Qt.Unchecked)
|
||||||
|
|
||||||
|
def get_boolean_value(self):
|
||||||
|
'''
|
||||||
|
Return a boolean value indicating whether checkbox is checked
|
||||||
|
If this is a tristate checkbox, a partially checked value is returned as None
|
||||||
|
'''
|
||||||
|
if self.checkState() == Qt.PartiallyChecked:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return self.checkState() == Qt.Checked
|
||||||
|
|
||||||
|
|
||||||
|
class TextIconWidgetItem(QTableWidgetItem):
|
||||||
|
|
||||||
|
def __init__(self, text, icon):
|
||||||
|
QTableWidgetItem.__init__(self, text)
|
||||||
|
if icon:
|
||||||
|
self.setIcon(icon)
|
||||||
|
|
||||||
|
|
||||||
|
class ReadOnlyTextIconWidgetItem(ReadOnlyTableWidgetItem):
|
||||||
|
|
||||||
|
def __init__(self, text, icon):
|
||||||
|
ReadOnlyTableWidgetItem.__init__(self, text)
|
||||||
|
if icon:
|
||||||
|
self.setIcon(icon)
|
||||||
|
|
||||||
|
|
||||||
|
class ReadOnlyLineEdit(QLineEdit):
|
||||||
|
|
||||||
|
def __init__(self, text, parent):
|
||||||
|
if text is None:
|
||||||
|
text = ''
|
||||||
|
QLineEdit.__init__(self, text, parent)
|
||||||
|
self.setEnabled(False)
|
||||||
|
|
||||||
|
|
||||||
|
class NumericLineEdit(QLineEdit):
|
||||||
|
'''
|
||||||
|
Allows a numeric value up to two decimal places, or an integer
|
||||||
|
'''
|
||||||
|
def __init__(self, *args):
|
||||||
|
QLineEdit.__init__(self, *args)
|
||||||
|
self.setValidator(QRegExpValidator(QRegExp(r'(^\d*\.[\d]{1,2}$)|(^[1-9]\d*[\.]$)'), self))
|
||||||
|
|
||||||
|
|
||||||
|
class KeyValueComboBox(QComboBox):
|
||||||
|
|
||||||
|
def __init__(self, parent, values, selected_key):
|
||||||
|
QComboBox.__init__(self, parent)
|
||||||
|
self.values = values
|
||||||
|
self.populate_combo(selected_key)
|
||||||
|
|
||||||
|
def populate_combo(self, selected_key):
|
||||||
|
self.clear()
|
||||||
|
selected_idx = idx = -1
|
||||||
|
for key, value in self.values.iteritems():
|
||||||
|
idx = idx + 1
|
||||||
|
self.addItem(value)
|
||||||
|
if key == selected_key:
|
||||||
|
selected_idx = idx
|
||||||
|
self.setCurrentIndex(selected_idx)
|
||||||
|
|
||||||
|
def selected_key(self):
|
||||||
|
for key, value in self.values.iteritems():
|
||||||
|
if value == unicode(self.currentText()).strip():
|
||||||
|
return key
|
||||||
|
|
||||||
|
|
||||||
|
class KeyComboBox(QComboBox):
|
||||||
|
|
||||||
|
def __init__(self, parent, values, selected_key):
|
||||||
|
QComboBox.__init__(self, parent)
|
||||||
|
self.values = values
|
||||||
|
self.populate_combo(selected_key)
|
||||||
|
|
||||||
|
def populate_combo(self, selected_key):
|
||||||
|
self.clear()
|
||||||
|
selected_idx = idx = -1
|
||||||
|
for key in sorted(self.values.keys()):
|
||||||
|
idx = idx + 1
|
||||||
|
self.addItem(key)
|
||||||
|
if key == selected_key:
|
||||||
|
selected_idx = idx
|
||||||
|
self.setCurrentIndex(selected_idx)
|
||||||
|
|
||||||
|
def selected_key(self):
|
||||||
|
for key, value in self.values.iteritems():
|
||||||
|
if key == unicode(self.currentText()).strip():
|
||||||
|
return key
|
||||||
|
|
||||||
|
|
||||||
|
class CustomColumnComboBox(QComboBox):
|
||||||
|
|
||||||
|
def __init__(self, parent, custom_columns={}, selected_column='', initial_items=['']):
|
||||||
|
QComboBox.__init__(self, parent)
|
||||||
|
self.populate_combo(custom_columns, selected_column, initial_items)
|
||||||
|
|
||||||
|
def populate_combo(self, custom_columns, selected_column, initial_items=['']):
|
||||||
|
self.clear()
|
||||||
|
self.column_names = list(initial_items)
|
||||||
|
if len(initial_items) > 0:
|
||||||
|
self.addItems(initial_items)
|
||||||
|
selected_idx = 0
|
||||||
|
for idx, value in enumerate(initial_items):
|
||||||
|
if value == selected_column:
|
||||||
|
selected_idx = idx
|
||||||
|
for key in sorted(custom_columns.keys()):
|
||||||
|
self.column_names.append(key)
|
||||||
|
self.addItem('%s (%s)'%(key, custom_columns[key]['name']))
|
||||||
|
if key == selected_column:
|
||||||
|
selected_idx = len(self.column_names) - 1
|
||||||
|
self.setCurrentIndex(selected_idx)
|
||||||
|
|
||||||
|
def get_selected_column(self):
|
||||||
|
return self.column_names[self.currentIndex()]
|
||||||
|
|
||||||
|
|
||||||
|
class KeyboardConfigDialog(SizePersistedDialog):
|
||||||
|
'''
|
||||||
|
This dialog is used to allow editing of keyboard shortcuts.
|
||||||
|
'''
|
||||||
|
def __init__(self, gui, group_name):
|
||||||
|
SizePersistedDialog.__init__(self, gui, 'Keyboard shortcut dialog')
|
||||||
|
self.gui = gui
|
||||||
|
self.setWindowTitle('Keyboard shortcuts')
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
self.keyboard_widget = ShortcutConfig(self)
|
||||||
|
layout.addWidget(self.keyboard_widget)
|
||||||
|
self.group_name = group_name
|
||||||
|
|
||||||
|
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||||
|
button_box.accepted.connect(self.commit)
|
||||||
|
button_box.rejected.connect(self.reject)
|
||||||
|
layout.addWidget(button_box)
|
||||||
|
|
||||||
|
# Cause our dialog size to be restored from prefs or created on first usage
|
||||||
|
self.resize_dialog()
|
||||||
|
self.initialize()
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
self.keyboard_widget.initialize(self.gui.keyboard)
|
||||||
|
self.keyboard_widget.highlight_group(self.group_name)
|
||||||
|
|
||||||
|
def commit(self):
|
||||||
|
self.keyboard_widget.commit()
|
||||||
|
self.accept()
|
||||||
|
|
||||||
|
|
||||||
|
class ProgressBar(QDialog):
|
||||||
|
def __init__(self, parent=None, max_items=100, window_title='Progress Bar',
|
||||||
|
label='Label goes here', on_top=False):
|
||||||
|
if on_top:
|
||||||
|
QDialog.__init__(self, parent=parent, flags=Qt.WindowStaysOnTopHint)
|
||||||
|
else:
|
||||||
|
QDialog.__init__(self, parent=parent)
|
||||||
|
self.application = Application
|
||||||
|
self.setWindowTitle(window_title)
|
||||||
|
self.l = QVBoxLayout(self)
|
||||||
|
self.setLayout(self.l)
|
||||||
|
|
||||||
|
self.label = QLabel(label)
|
||||||
|
self.label.setAlignment(Qt.AlignHCenter)
|
||||||
|
self.l.addWidget(self.label)
|
||||||
|
|
||||||
|
self.progressBar = QProgressBar(self)
|
||||||
|
self.progressBar.setRange(0, max_items)
|
||||||
|
self.progressBar.setValue(0)
|
||||||
|
self.l.addWidget(self.progressBar)
|
||||||
|
|
||||||
|
def increment(self):
|
||||||
|
self.progressBar.setValue(self.progressBar.value() + 1)
|
||||||
|
self.refresh()
|
||||||
|
|
||||||
|
def refresh(self):
|
||||||
|
self.application.processEvents()
|
||||||
|
|
||||||
|
def set_label(self, value):
|
||||||
|
self.label.setText(value)
|
||||||
|
self.refresh()
|
||||||
|
|
||||||
|
def set_maximum(self, value):
|
||||||
|
self.progressBar.setMaximum(value)
|
||||||
|
self.refresh()
|
||||||
|
|
||||||
|
def set_value(self, value):
|
||||||
|
self.progressBar.setValue(value)
|
||||||
|
self.refresh()
|
||||||
|
|
||||||
|
def convert_kobo_date(kobo_date):
|
||||||
|
from calibre.utils.date import utc_tz
|
||||||
|
|
||||||
|
try:
|
||||||
|
converted_date = datetime.strptime(kobo_date, "%Y-%m-%dT%H:%M:%S.%f")
|
||||||
|
converted_date = datetime.strptime(kobo_date[0:19], "%Y-%m-%dT%H:%M:%S")
|
||||||
|
converted_date = converted_date.replace(tzinfo=utc_tz)
|
||||||
|
# debug_print("convert_kobo_date - '%Y-%m-%dT%H:%M:%S.%f' - kobo_date={0}'".format(kobo_date))
|
||||||
|
except:
|
||||||
|
try:
|
||||||
|
converted_date = datetime.strptime(kobo_date, "%Y-%m-%dT%H:%M:%S%+00:00")
|
||||||
|
# debug_print("convert_kobo_date - '%Y-%m-%dT%H:%M:%S+00:00' - kobo_date=%s' - kobo_date={0}'".format(kobo_date))
|
||||||
|
except:
|
||||||
|
try:
|
||||||
|
converted_date = datetime.strptime(kobo_date.split('+')[0], "%Y-%m-%dT%H:%M:%S")
|
||||||
|
converted_date = converted_date.replace(tzinfo=utc_tz)
|
||||||
|
# debug_print("convert_kobo_date - '%Y-%m-%dT%H:%M:%S' - kobo_date={0}'".format(kobo_date))
|
||||||
|
except:
|
||||||
|
try:
|
||||||
|
converted_date = datetime.strptime(kobo_date.split('+')[0], "%Y-%m-%d")
|
||||||
|
converted_date = converted_date.replace(tzinfo=utc_tz)
|
||||||
|
# debug_print("convert_kobo_date - '%Y-%m-%d' - kobo_date={0}'".format(kobo_date))
|
||||||
|
except:
|
||||||
|
try:
|
||||||
|
from calibre.utils.date import parse_date
|
||||||
|
converted_date = parse_date(kobo_date, assume_utc=True)
|
||||||
|
# debug_print("convert_kobo_date - parse_date - kobo_date=%s' - kobo_date={0}'".format(kobo_date))
|
||||||
|
except:
|
||||||
|
# try:
|
||||||
|
# converted_date = time.gmtime(os.path.getctime(self.path))
|
||||||
|
# debug_print("convert_kobo_date - time.gmtime(os.path.getctime(self.path)) - kobo_date={0}'".format(kobo_date))
|
||||||
|
# except:
|
||||||
|
converted_date = time.gmtime()
|
||||||
|
debug_print("convert_kobo_date - time.gmtime() - kobo_date={0}'".format(kobo_date))
|
||||||
|
return converted_date
|
@ -0,0 +1,40 @@
|
|||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
from __future__ import (unicode_literals, division, absolute_import,
|
||||||
|
print_function)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from PyQt5.Qt import (QWidget, QLabel, QVBoxLayout, QHBoxLayout, QComboBox)
|
||||||
|
except ImportError:
|
||||||
|
from PyQt4.Qt import (QWidget, QLabel, QVBoxLayout, QHBoxLayout, QComboBox)
|
||||||
|
|
||||||
|
from calibre.utils.config import JSONConfig, config_dir
|
||||||
|
|
||||||
|
plugin_prefs = JSONConfig('plugins/obok_dedrm_prefs')
|
||||||
|
plugin_prefs.defaults['finding_homes_for_formats'] = 'Ask'
|
||||||
|
|
||||||
|
from calibre_plugins.obok_dedrm.utilities import (debug_print)
|
||||||
|
try:
|
||||||
|
debug_print("obok::config.py - loading translations")
|
||||||
|
load_translations()
|
||||||
|
except NameError:
|
||||||
|
debug_print("obok::config.py - exception when loading translations")
|
||||||
|
pass # load_translations() added in calibre 1.9
|
||||||
|
|
||||||
|
class ConfigWidget(QWidget):
|
||||||
|
def __init__(self, plugin_action):
|
||||||
|
QWidget.__init__(self)
|
||||||
|
self.plugin_action = plugin_action
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
combo_label = QLabel(_('When should Obok try to insert EPUBs into existing calibre entries?'), self)
|
||||||
|
layout.addWidget(combo_label)
|
||||||
|
self.find_homes = QComboBox()
|
||||||
|
self.find_homes.setToolTip(_('<p>Default behavior when duplicates are detected. None of the choices will cause calibre ebooks to be overwritten'))
|
||||||
|
layout.addWidget(self.find_homes)
|
||||||
|
self.find_homes.addItems([_('Ask'), _('Always'), _('Never')])
|
||||||
|
index = self.find_homes.findText(plugin_prefs['finding_homes_for_formats'])
|
||||||
|
self.find_homes.setCurrentIndex(index)
|
||||||
|
|
||||||
|
def save_settings(self):
|
||||||
|
plugin_prefs['finding_homes_for_formats'] = unicode(self.find_homes.currentText())
|
@ -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 <LL@li.org>\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 ""
|
@ -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',' ')#.replace('\n', '<br/>')
|
||||||
|
html = html.replace('> ','> ')
|
||||||
|
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)
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 2.8 KiB |
@ -0,0 +1,4 @@
|
|||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
@ -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
|
@ -0,0 +1,482 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
# Version 3.05 October 2014
|
||||||
|
# Identifies DRM-free books in the dialog
|
||||||
|
#
|
||||||
|
# Version 3.04 September 2014
|
||||||
|
# Handles DRM-free books as well (sometimes Kobo Library doesn't
|
||||||
|
# show download link for DRM-free books)
|
||||||
|
#
|
||||||
|
# Version 3.03 August 2014
|
||||||
|
# If PyCrypto is unavailable try to use libcrypto for AES_ECB.
|
||||||
|
#
|
||||||
|
# Version 3.02 August 2014
|
||||||
|
# Relax checking of application/xhtml+xml and image/jpeg content.
|
||||||
|
#
|
||||||
|
# Version 3.01 June 2014
|
||||||
|
# Check image/jpeg as well as application/xhtml+xml content. Fix typo
|
||||||
|
# in Windows ipconfig parsing.
|
||||||
|
#
|
||||||
|
# Version 3.0 June 2014
|
||||||
|
# Made portable for Mac and Windows, and the only module dependency
|
||||||
|
# not part of python core is PyCrypto. Major code cleanup/rewrite.
|
||||||
|
# No longer tries the first MAC address; tries them all if it detects
|
||||||
|
# the decryption failed.
|
||||||
|
#
|
||||||
|
# Updated September 2013 by Anon
|
||||||
|
# Version 2.02
|
||||||
|
# Incorporated minor fixes posted at Apprentice Alf's.
|
||||||
|
#
|
||||||
|
# Updates July 2012 by Michael Newton
|
||||||
|
# PWSD ID is no longer a MAC address, but should always
|
||||||
|
# be stored in the registry. Script now works with OS X
|
||||||
|
# and checks plist for values instead of registry. Must
|
||||||
|
# have biplist installed for OS X support.
|
||||||
|
#
|
||||||
|
# Original comments left below; note the "AUTOPSY" is inaccurate. See
|
||||||
|
# KoboLibrary.userkeys and KoboFile.decrypt()
|
||||||
|
#
|
||||||
|
##########################################################
|
||||||
|
# KOBO DRM CRACK BY #
|
||||||
|
# PHYSISTICATED #
|
||||||
|
##########################################################
|
||||||
|
# This app was made for Python 2.7 on Windows 32-bit
|
||||||
|
#
|
||||||
|
# This app needs pycrypto - get from here:
|
||||||
|
# http://www.voidspace.org.uk/python/modules.shtml
|
||||||
|
#
|
||||||
|
# Usage: obok.py
|
||||||
|
# Choose the book you want to decrypt
|
||||||
|
#
|
||||||
|
# Shouts to my krew - you know who you are - and one in
|
||||||
|
# particular who gave me a lot of help with this - thank
|
||||||
|
# you so much!
|
||||||
|
#
|
||||||
|
# Kopimi /K\
|
||||||
|
# Keep sharing, keep copying, but remember that nothing is
|
||||||
|
# for free - make sure you compensate your favorite
|
||||||
|
# authors - and cut out the middle man whenever possible
|
||||||
|
# ;) ;) ;)
|
||||||
|
#
|
||||||
|
# DRM AUTOPSY
|
||||||
|
# The Kobo DRM was incredibly easy to crack, but it took
|
||||||
|
# me months to get around to making this. Here's the
|
||||||
|
# basics of how it works:
|
||||||
|
# 1: Get MAC address of first NIC in ipconfig (sometimes
|
||||||
|
# stored in registry as pwsdid)
|
||||||
|
# 2: Get user ID (stored in tons of places, this gets it
|
||||||
|
# from HKEY_CURRENT_USER\Software\Kobo\Kobo Desktop
|
||||||
|
# Edition\Browser\cookies)
|
||||||
|
# 3: Concatenate and SHA256, take the second half - this
|
||||||
|
# is your master key
|
||||||
|
# 4: Open %LOCALAPPDATA%\Kobo Desktop Editions\Kobo.sqlite
|
||||||
|
# and dump content_keys
|
||||||
|
# 5: Unbase64 the keys, then decode these with the master
|
||||||
|
# key - these are your page keys
|
||||||
|
# 6: Unzip EPUB of your choice, decrypt each page with its
|
||||||
|
# page key, then zip back up again
|
||||||
|
#
|
||||||
|
# WHY USE THIS WHEN INEPT WORKS FINE? (adobe DRM stripper)
|
||||||
|
# Inept works very well, but authors on Kobo can choose
|
||||||
|
# what DRM they want to use - and some have chosen not to
|
||||||
|
# let people download them with Adobe Digital Editions -
|
||||||
|
# they would rather lock you into a single platform.
|
||||||
|
#
|
||||||
|
# With Obok, you can sync Kobo Desktop, decrypt all your
|
||||||
|
# ebooks, and then use them on whatever device you want
|
||||||
|
# - you bought them, you own them, you can do what you
|
||||||
|
# like with them.
|
||||||
|
#
|
||||||
|
# Obok is Kobo backwards, but it is also means "next to"
|
||||||
|
# in Polish.
|
||||||
|
# When you buy a real book, it is right next to you. You
|
||||||
|
# can read it at home, at work, on a train, you can lend
|
||||||
|
# it to a friend, you can scribble on it, and add your own
|
||||||
|
# explanations/translations.
|
||||||
|
#
|
||||||
|
# Obok gives you this power over your ebooks - no longer
|
||||||
|
# are you restricted to one device. This allows you to
|
||||||
|
# embed foreign fonts into your books, as older Kobo's
|
||||||
|
# can't display them properly. You can read your books
|
||||||
|
# on your phones, in different PC readers, and different
|
||||||
|
# ereader devices. You can share them with your friends
|
||||||
|
# too, if you like - you can do that with a real book
|
||||||
|
# after all.
|
||||||
|
#
|
||||||
|
"""Manage all Kobo books, either encrypted or DRM-free."""
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sqlite3
|
||||||
|
import base64
|
||||||
|
import binascii
|
||||||
|
import re
|
||||||
|
import zipfile
|
||||||
|
import hashlib
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
import string
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
class ENCRYPTIONError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _load_crypto_libcrypto():
|
||||||
|
from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \
|
||||||
|
Structure, c_ulong, create_string_buffer, cast
|
||||||
|
from ctypes.util import find_library
|
||||||
|
|
||||||
|
if sys.platform.startswith('win'):
|
||||||
|
libcrypto = find_library('libeay32')
|
||||||
|
else:
|
||||||
|
libcrypto = find_library('crypto')
|
||||||
|
|
||||||
|
if libcrypto is None:
|
||||||
|
raise ENCRYPTIONError('libcrypto not found')
|
||||||
|
libcrypto = CDLL(libcrypto)
|
||||||
|
|
||||||
|
AES_MAXNR = 14
|
||||||
|
|
||||||
|
c_char_pp = POINTER(c_char_p)
|
||||||
|
c_int_p = POINTER(c_int)
|
||||||
|
|
||||||
|
class AES_KEY(Structure):
|
||||||
|
_fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))),
|
||||||
|
('rounds', c_int)]
|
||||||
|
AES_KEY_p = POINTER(AES_KEY)
|
||||||
|
|
||||||
|
def F(restype, name, argtypes):
|
||||||
|
func = getattr(libcrypto, name)
|
||||||
|
func.restype = restype
|
||||||
|
func.argtypes = argtypes
|
||||||
|
return func
|
||||||
|
|
||||||
|
AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',
|
||||||
|
[c_char_p, c_int, AES_KEY_p])
|
||||||
|
AES_ecb_encrypt = F(None, 'AES_ecb_encrypt',
|
||||||
|
[c_char_p, c_char_p, AES_KEY_p, c_int])
|
||||||
|
|
||||||
|
class AES(object):
|
||||||
|
def __init__(self, userkey):
|
||||||
|
self._blocksize = len(userkey)
|
||||||
|
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
|
||||||
|
raise ENCRYPTIONError(_('AES improper key used'))
|
||||||
|
return
|
||||||
|
key = self._key = AES_KEY()
|
||||||
|
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key)
|
||||||
|
if rv < 0:
|
||||||
|
raise ENCRYPTIONError(_('Failed to initialize AES key'))
|
||||||
|
|
||||||
|
def decrypt(self, data):
|
||||||
|
clear = ''
|
||||||
|
for i in range(0, len(data), 16):
|
||||||
|
out = create_string_buffer(16)
|
||||||
|
rv = AES_ecb_encrypt(data[i:i+16], out, self._key, 0)
|
||||||
|
if rv == 0:
|
||||||
|
raise ENCRYPTIONError(_('AES decryption failed'))
|
||||||
|
clear += out.raw
|
||||||
|
return clear
|
||||||
|
|
||||||
|
return AES
|
||||||
|
|
||||||
|
def _load_crypto_pycrypto():
|
||||||
|
from Crypto.Cipher import AES as _AES
|
||||||
|
class AES(object):
|
||||||
|
def __init__(self, key):
|
||||||
|
self._aes = _AES.new(key, _AES.MODE_ECB)
|
||||||
|
|
||||||
|
def decrypt(self, data):
|
||||||
|
return self._aes.decrypt(data)
|
||||||
|
|
||||||
|
return AES
|
||||||
|
|
||||||
|
def _load_crypto():
|
||||||
|
AES = None
|
||||||
|
cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto)
|
||||||
|
for loader in cryptolist:
|
||||||
|
try:
|
||||||
|
AES = loader()
|
||||||
|
break
|
||||||
|
except (ImportError, ENCRYPTIONError):
|
||||||
|
pass
|
||||||
|
return AES
|
||||||
|
|
||||||
|
AES = _load_crypto()
|
||||||
|
|
||||||
|
class KoboLibrary(object):
|
||||||
|
"""The Kobo library.
|
||||||
|
|
||||||
|
This class represents all the information available from the data
|
||||||
|
written by the Kobo Desktop Edition application, including the list
|
||||||
|
of books, their titles, and the user's encryption key(s)."""
|
||||||
|
|
||||||
|
def __init__ (self):
|
||||||
|
if sys.platform.startswith('win'):
|
||||||
|
if sys.getwindowsversion().major > 5:
|
||||||
|
self.kobodir = os.environ['LOCALAPPDATA']
|
||||||
|
else:
|
||||||
|
self.kobodir = os.path.join(os.environ['USERPROFILE'], 'Local Settings', 'Application Data')
|
||||||
|
self.kobodir = os.path.join(self.kobodir, 'Kobo', 'Kobo Desktop Edition')
|
||||||
|
elif sys.platform.startswith('darwin'):
|
||||||
|
self.kobodir = os.path.join(os.environ['HOME'], 'Library', 'Application Support', 'Kobo', 'Kobo Desktop Edition')
|
||||||
|
self.bookdir = os.path.join(self.kobodir, 'kepub')
|
||||||
|
kobodb = os.path.join(self.kobodir, 'Kobo.sqlite')
|
||||||
|
self.__sqlite = sqlite3.connect(kobodb)
|
||||||
|
self.__cursor = self.__sqlite.cursor()
|
||||||
|
self._userkeys = []
|
||||||
|
self._books = []
|
||||||
|
self._volumeID = []
|
||||||
|
|
||||||
|
def close (self):
|
||||||
|
"""Closes the database used by the library."""
|
||||||
|
self.__cursor.close()
|
||||||
|
self.__sqlite.close()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def userkeys (self):
|
||||||
|
"""The list of potential userkeys being used by this library.
|
||||||
|
Only one of these will be valid.
|
||||||
|
"""
|
||||||
|
if len(self._userkeys) != 0:
|
||||||
|
return self._userkeys
|
||||||
|
userid = self.__getuserid()
|
||||||
|
for macaddr in self.__getmacaddrs():
|
||||||
|
self._userkeys.append(self.__getuserkey(macaddr, userid))
|
||||||
|
return self._userkeys
|
||||||
|
|
||||||
|
@property
|
||||||
|
def books (self):
|
||||||
|
"""The list of KoboBook objects in the library."""
|
||||||
|
if len(self._books) != 0:
|
||||||
|
return self._books
|
||||||
|
"""Drm-ed kepub"""
|
||||||
|
for row in self.__cursor.execute('SELECT DISTINCT volumeid, Title, Attribution, Series FROM content_keys, content WHERE contentid = volumeid'):
|
||||||
|
self._books.append(KoboBook(row[0], row[1], self.__bookfile(row[0]), 'kepub', self.__cursor, author=row[2], series=row[3]))
|
||||||
|
self._volumeID.append(row[0])
|
||||||
|
"""Drm-free"""
|
||||||
|
for f in os.listdir(self.bookdir):
|
||||||
|
if(f not in self._volumeID):
|
||||||
|
row = self.__cursor.execute("SELECT Title, Attribution, Series FROM content WHERE ContentID = '" + f + "'").fetchone()
|
||||||
|
if row is not None:
|
||||||
|
fTitle = row[0]
|
||||||
|
self._books.append(KoboBook(f, fTitle, self.__bookfile(f), 'drm-free', self.__cursor, author=row[1], series=row[2]))
|
||||||
|
self._volumeID.append(f)
|
||||||
|
"""Sort"""
|
||||||
|
self._books.sort(key=lambda x: x.title)
|
||||||
|
return self._books
|
||||||
|
|
||||||
|
def __bookfile (self, volumeid):
|
||||||
|
"""The filename needed to open a given book."""
|
||||||
|
return os.path.join(self.kobodir, 'kepub', volumeid)
|
||||||
|
|
||||||
|
def __getmacaddrs (self):
|
||||||
|
"""The list of all MAC addresses on this machine."""
|
||||||
|
macaddrs = []
|
||||||
|
if sys.platform.startswith('win'):
|
||||||
|
c = re.compile('\s(' + '[0-9a-f]{2}-' * 5 + '[0-9a-f]{2})(\s|$)', re.IGNORECASE)
|
||||||
|
for line in os.popen('ipconfig /all'):
|
||||||
|
m = c.search(line)
|
||||||
|
if m:
|
||||||
|
macaddrs.append(re.sub("-", ":", m.group(1)).upper())
|
||||||
|
elif sys.platform.startswith('darwin'):
|
||||||
|
c = re.compile('\s(' + '[0-9a-f]{2}:' * 5 + '[0-9a-f]{2})(\s|$)', re.IGNORECASE)
|
||||||
|
output = subprocess.check_output('/sbin/ifconfig -a', shell=True)
|
||||||
|
matches = c.findall(output)
|
||||||
|
for m in matches:
|
||||||
|
# print "m:",m[0]
|
||||||
|
macaddrs.append(m[0].upper())
|
||||||
|
return macaddrs
|
||||||
|
|
||||||
|
def __getuserid (self):
|
||||||
|
return self.__cursor.execute('SELECT UserID FROM user WHERE HasMadePurchase = "true"').fetchone()[0]
|
||||||
|
|
||||||
|
def __getuserkey (self, macaddr, userid):
|
||||||
|
deviceid = hashlib.sha256('NoCanLook' + macaddr).hexdigest()
|
||||||
|
userkey = hashlib.sha256(deviceid + userid).hexdigest()
|
||||||
|
return binascii.a2b_hex(userkey[32:])
|
||||||
|
|
||||||
|
class KoboBook(object):
|
||||||
|
"""A Kobo book.
|
||||||
|
|
||||||
|
A Kobo book contains a number of unencrypted and encrypted files.
|
||||||
|
This class provides a list of the encrypted files.
|
||||||
|
|
||||||
|
Each book has the following instance variables:
|
||||||
|
volumeid - a UUID which uniquely refers to the book in this library.
|
||||||
|
title - the human-readable book title.
|
||||||
|
filename - the complete path and filename of the book.
|
||||||
|
type - either kepub or drm-free"""
|
||||||
|
def __init__ (self, volumeid, title, filename, type, cursor, author=None, series=None):
|
||||||
|
self.volumeid = volumeid
|
||||||
|
self.title = title
|
||||||
|
self.author = author
|
||||||
|
self.series = series
|
||||||
|
self.series_index = None
|
||||||
|
self.filename = filename
|
||||||
|
self.type = type
|
||||||
|
self.__cursor = cursor
|
||||||
|
self._encryptedfiles = {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def encryptedfiles (self):
|
||||||
|
"""A dictionary of KoboFiles inside the book.
|
||||||
|
|
||||||
|
The dictionary keys are the relative pathnames, which are
|
||||||
|
the same as the pathnames inside the book 'zip' file."""
|
||||||
|
if (self.type == 'drm-free'):
|
||||||
|
return self._encryptedfiles
|
||||||
|
if len(self._encryptedfiles) != 0:
|
||||||
|
return self._encryptedfiles
|
||||||
|
# Read the list of encrypted files from the DB
|
||||||
|
for row in self.__cursor.execute('SELECT elementid,elementkey FROM content_keys,content WHERE volumeid = ? AND volumeid = contentid',(self.volumeid,)):
|
||||||
|
self._encryptedfiles[row[0]] = KoboFile(row[0], None, base64.b64decode(row[1]))
|
||||||
|
|
||||||
|
# Read the list of files from the kepub OPF manifest so that
|
||||||
|
# we can get their proper MIME type.
|
||||||
|
# NOTE: this requires that the OPF file is unencrypted!
|
||||||
|
zin = zipfile.ZipFile(self.filename, "r")
|
||||||
|
xmlns = {
|
||||||
|
'ocf': 'urn:oasis:names:tc:opendocument:xmlns:container',
|
||||||
|
'opf': 'http://www.idpf.org/2007/opf'
|
||||||
|
}
|
||||||
|
ocf = ET.fromstring(zin.read('META-INF/container.xml'))
|
||||||
|
opffile = ocf.find('.//ocf:rootfile', xmlns).attrib['full-path']
|
||||||
|
basedir = re.sub('[^/]+$', '', opffile)
|
||||||
|
opf = ET.fromstring(zin.read(opffile))
|
||||||
|
zin.close()
|
||||||
|
|
||||||
|
c = re.compile('/')
|
||||||
|
for item in opf.findall('.//opf:item', xmlns):
|
||||||
|
mimetype = item.attrib['media-type']
|
||||||
|
|
||||||
|
# Convert relative URIs
|
||||||
|
href = item.attrib['href']
|
||||||
|
if not c.match(href):
|
||||||
|
href = string.join((basedir, href), '')
|
||||||
|
|
||||||
|
# Update books we've found from the DB.
|
||||||
|
if href in self._encryptedfiles:
|
||||||
|
self._encryptedfiles[href].mimetype = mimetype
|
||||||
|
return self._encryptedfiles
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_drm (self):
|
||||||
|
return not self.type == 'drm-free'
|
||||||
|
|
||||||
|
|
||||||
|
class KoboFile(object):
|
||||||
|
"""An encrypted file in a KoboBook.
|
||||||
|
|
||||||
|
Each file has the following instance variables:
|
||||||
|
filename - the relative pathname inside the book zip file.
|
||||||
|
mimetype - the file's MIME type, e.g. 'image/jpeg'
|
||||||
|
key - the encrypted page key."""
|
||||||
|
|
||||||
|
def __init__ (self, filename, mimetype, key):
|
||||||
|
self.filename = filename
|
||||||
|
self.mimetype = mimetype
|
||||||
|
self.key = key
|
||||||
|
def decrypt (self, userkey, contents):
|
||||||
|
"""
|
||||||
|
Decrypt the contents using the provided user key and the
|
||||||
|
file page key. The caller must determine if the decrypted
|
||||||
|
data is correct."""
|
||||||
|
# The userkey decrypts the page key (self.key)
|
||||||
|
keyenc = AES(userkey)
|
||||||
|
decryptedkey = keyenc.decrypt(self.key)
|
||||||
|
# The decrypted page key decrypts the content
|
||||||
|
pageenc = AES(decryptedkey)
|
||||||
|
return self.__removeaespadding(pageenc.decrypt(contents))
|
||||||
|
|
||||||
|
def check (self, contents):
|
||||||
|
"""
|
||||||
|
If the contents uses some known MIME types, check if it
|
||||||
|
conforms to the type. Throw a ValueError exception if not.
|
||||||
|
If the contents uses an uncheckable MIME type, don't check
|
||||||
|
it and don't throw an exception.
|
||||||
|
Returns True if the content was checked, False if it was not
|
||||||
|
checked."""
|
||||||
|
if self.mimetype == 'application/xhtml+xml':
|
||||||
|
if contents[:5]=="<?xml":
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print "Bad XML: ",contents[:5]
|
||||||
|
raise ValueError
|
||||||
|
if self.mimetype == 'image/jpeg':
|
||||||
|
if contents[:3] == '\xff\xd8\xff':
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print "Bad JPEG: ", contents[:3].encode('hex')
|
||||||
|
raise ValueError()
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __removeaespadding (self, contents):
|
||||||
|
"""
|
||||||
|
Remove the trailing padding, using what appears to be the CMS
|
||||||
|
algorithm from RFC 5652 6.3"""
|
||||||
|
lastchar = binascii.b2a_hex(contents[-1:])
|
||||||
|
strlen = int(lastchar, 16)
|
||||||
|
padding = strlen
|
||||||
|
if strlen == 1:
|
||||||
|
return contents[:-1]
|
||||||
|
if strlen < 16:
|
||||||
|
for i in range(strlen):
|
||||||
|
testchar = binascii.b2a_hex(contents[-strlen:-(strlen-1)])
|
||||||
|
if testchar != lastchar:
|
||||||
|
padding = 0
|
||||||
|
if padding > 0:
|
||||||
|
contents = contents[:-padding]
|
||||||
|
return contents
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
|
||||||
|
lib = KoboLibrary()
|
||||||
|
|
||||||
|
for i, book in enumerate(lib.books):
|
||||||
|
print ('%d: %s' % (i + 1, book.title)).encode('ascii', 'ignore')
|
||||||
|
|
||||||
|
num_string = raw_input("Convert book number... ")
|
||||||
|
try:
|
||||||
|
num = int(num_string)
|
||||||
|
book = lib.books[num - 1]
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
exit()
|
||||||
|
|
||||||
|
print "Converting", book.title
|
||||||
|
|
||||||
|
zin = zipfile.ZipFile(book.filename, "r")
|
||||||
|
# make filename out of Unicode alphanumeric and whitespace equivalents from title
|
||||||
|
outname = "%s.epub" % (re.sub('[^\s\w]', '', book.title, 0, re.UNICODE))
|
||||||
|
|
||||||
|
if (book.type == 'drm-free'):
|
||||||
|
print "DRM-free book, conversion is not needed"
|
||||||
|
shutil.copyfile(book.filename, outname)
|
||||||
|
print "Book saved as", os.path.join(os.getcwd(), outname)
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
result = 1
|
||||||
|
for userkey in lib.userkeys:
|
||||||
|
# print "Trying key: ",userkey.encode('hex_codec')
|
||||||
|
confirmedGood = False
|
||||||
|
try:
|
||||||
|
zout = zipfile.ZipFile(outname, "w", zipfile.ZIP_DEFLATED)
|
||||||
|
for filename in zin.namelist():
|
||||||
|
contents = zin.read(filename)
|
||||||
|
if filename in book.encryptedfiles:
|
||||||
|
file = book.encryptedfiles[filename]
|
||||||
|
contents = file.decrypt(userkey, contents)
|
||||||
|
# Parse failures mean the key is probably wrong.
|
||||||
|
if not confirmedGood:
|
||||||
|
confirmedGood = file.check(contents)
|
||||||
|
zout.writestr(filename, contents)
|
||||||
|
zout.close()
|
||||||
|
print "Book saved as", os.path.join(os.getcwd(), outname)
|
||||||
|
result = 0
|
||||||
|
break
|
||||||
|
except ValueError:
|
||||||
|
print "Decryption failed, trying next key"
|
||||||
|
zout.close()
|
||||||
|
os.remove(outname)
|
||||||
|
|
||||||
|
zin.close()
|
||||||
|
lib.close()
|
||||||
|
exit(result)
|
@ -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."
|
@ -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 <LL@li.org>\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 ""
|
@ -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"
|
@ -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."
|
@ -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)
|
Loading…
Reference in New Issue