obok 3.1.1 plugin unzipped

pull/6/head
Apprentice Alf 9 years ago
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','&nbsp;&nbsp;&nbsp;&nbsp;')#.replace('\n', '<br/>')
html = html.replace('> ','>&nbsp;')
self.tb.setHtml('<div>{0}</div>'.format(html))
QApplication.restoreOverrideCursor()
l.addWidget(self.tb)
self.bb = QDialogButtonBox(QDialogButtonBox.Ok)
self.bb.accepted.connect(self.accept)
self.bb.rejected.connect(self.reject)
self.copy_button = self.bb.addButton(_('Copy to clipboard'),
self.bb.ActionRole)
self.copy_button.setIcon(QIcon(I('edit-copy.png')))
self.copy_button.clicked.connect(self.copy_to_clipboard)
l.addWidget(self.bb)
self.setModal(False)
self.resize(QSize(700, 500))
self.setWindowTitle(title)
self.setWindowIcon(QIcon(I('dialog_information.png')))
self.show()
def copy_to_clipboard(self):
txt = self.tb.toPlainText()
QApplication.clipboard().setText(txt)
class ResultsSummaryDialog(MessageBox):
def __init__(self, parent, title, msg, log='', det_msg=''):
'''
:param log: An HTML log
:param title: The title for this popup
:param msg: The msg to display
:param det_msg: Detailed message
'''
MessageBox.__init__(self, MessageBox.INFO, title, msg,
det_msg=det_msg, show_copy_button=False,
parent=parent)
self.log = log
self.vlb = self.bb.addButton(_('View Report'), self.bb.ActionRole)
self.vlb.setIcon(QIcon(I('dialog_information.png')))
self.vlb.clicked.connect(self.show_log)
self.det_msg_toggle.setVisible(bool(det_msg))
self.vlb.setVisible(bool(log))
def show_log(self):
self.log_viewer = ViewLog(PLUGIN_NAME + ' v' + PLUGIN_VERSION, self.log,
parent=self)
class ReadOnlyTableWidgetItem(QTableWidgetItem):
def __init__(self, text):
if text is None:
text = ''
QTableWidgetItem.__init__(self, text, QTableWidgetItem.UserType)
self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
class AuthorTableWidgetItem(ReadOnlyTableWidgetItem):
def __init__(self, text, sort_key):
ReadOnlyTableWidgetItem.__init__(self, text)
self.sort_key = sort_key
#Qt uses a simple < check for sorting items, override this to use the sortKey
def __lt__(self, other):
return self.sort_key < other.sort_key
class SeriesTableWidgetItem(ReadOnlyTableWidgetItem):
def __init__(self, series, series_index=None):
display = ''
if series:
if series_index:
from calibre.ebooks.metadata import fmt_sidx
display = '%s [%s]' % (series, fmt_sidx(series_index))
self.sortKey = '%s%04d' % (series, series_index)
else:
display = series
self.sortKey = series
ReadOnlyTableWidgetItem.__init__(self, display)
class IconWidgetItem(ReadOnlyTableWidgetItem):
def __init__(self, text, icon, sort_key):
ReadOnlyTableWidgetItem.__init__(self, text)
if icon:
self.setIcon(icon)
self.sort_key = sort_key
#Qt uses a simple < check for sorting items, override this to use the sortKey
def __lt__(self, other):
return self.sort_key < other.sort_key
class NumericTableWidgetItem(QTableWidgetItem):
def __init__(self, number, is_read_only=False):
QTableWidgetItem.__init__(self, '', QTableWidgetItem.UserType)
self.setData(Qt.DisplayRole, number)
if is_read_only:
self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)

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,37 @@
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>Obok DeDRM Plugin Configuration</title>
</head>
<body>
<h1>Obok DeDRM Plugin</h1>
<h3>(version 3.1.0)</h3>
<h3> Headlines and links.</h3>
<p>Stuff.</p>
<p>And more stuff.</p>
<h3>Installation:</h3>
<p>The ususal method of Preferences -> Plugins -> Load plugin from file.</p>
<h3>Configuration:</h3>
<p>There is no configuration (other than to choose what menus to add obok to)</p>
<h3>Troubleshooting:</h3>
<p >If you find that its not working for you , you can save a lot of time by using the plugin with Calibre in debug mode. This will print out a lot of helpful info that can be copied into any online help requests.</p>
<p>Open a command prompt (terminal window) and type "calibre-debug -g" (without the quotes). Calibre will launch, and you can use the plugin the usual way. The debug info will be output to the original command prompt (terminal window). Copy the resulting output and paste it into the comment you make at my blog.</p>
<p><span class="bold">Note:</span> The Mac version of Calibre doesnt install the command line tools by default. If you go to the Preferences page and click on the miscellaneous button, youll find the option to install the command line tools.</p>
</body>
</html>

@ -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…
Cancel
Save