Compare commits

...

10 Commits

Author SHA1 Message Date
Apprentice Harper
00a5c4e1d1 Updated obok plugin readme 2015-10-05 07:49:48 +01:00
apprenticeharper
4ea0d81144 Change to unicode strings to fix stand-alone character encoding problems 2015-09-30 07:39:29 +01:00
Apprentice Harper
b1cccf4b25 Merge pull request #39 from norbusan/obok_linux
improvements in the obok device handling
2015-09-21 07:40:54 +01:00
Norbert Preining
fe6074949b obok.py: first try device, and only if that fails fall back to Desktop progs 2015-09-18 09:10:06 +09:00
Norbert Preining
2db7ee8894 obok_plugin:action.py - get serial from device if possible 2015-09-18 08:58:01 +09:00
Norbert Preining
93d8758462 get device path from calibre, and allow device usage on all platforms 2015-09-16 23:01:29 +09:00
Norbert Preining
f97bc078db add support for linux via device serials and reading from device 2015-09-14 14:12:22 +09:00
apprenticeharper
2e96db6cdc More changes to the obok cli interface for character encodings 2015-09-08 07:52:06 +01:00
apprenticeharper
0d530c0c46 Add encryption fixes from other scripts (SafeUnbuffered). 2015-09-07 08:12:45 +01:00
apprenticeharper
488924d443 Fix for kobo users who haven't yet bought a book. 2015-09-07 07:52:23 +01:00
7 changed files with 435 additions and 84 deletions

Binary file not shown.

View File

@@ -19,7 +19,7 @@ except NameError:
PLUGIN_NAME = 'Obok DeDRM' PLUGIN_NAME = 'Obok DeDRM'
PLUGIN_SAFE_NAME = PLUGIN_NAME.strip().lower().replace(' ', '_') PLUGIN_SAFE_NAME = PLUGIN_NAME.strip().lower().replace(' ', '_')
PLUGIN_DESCRIPTION = _('Removes DRM from Kobo kepubs and adds them to the library.') PLUGIN_DESCRIPTION = _('Removes DRM from Kobo kepubs and adds them to the library.')
PLUGIN_VERSION_TUPLE = (3, 1, 4) PLUGIN_VERSION_TUPLE = (3, 1, 6)
PLUGIN_VERSION = '.'.join([str(x) for x in PLUGIN_VERSION_TUPLE]) PLUGIN_VERSION = '.'.join([str(x) for x in PLUGIN_VERSION_TUPLE])
HELPFILE_NAME = PLUGIN_SAFE_NAME + '_Help.htm' HELPFILE_NAME = PLUGIN_SAFE_NAME + '_Help.htm'
PLUGIN_AUTHORS = 'Anon' PLUGIN_AUTHORS = 'Anon'
@@ -29,7 +29,7 @@ class ObokDeDRMAction(InterfaceActionBase):
name = PLUGIN_NAME name = PLUGIN_NAME
description = PLUGIN_DESCRIPTION description = PLUGIN_DESCRIPTION
supported_platforms = ['windows', 'osx'] supported_platforms = ['windows', 'osx', 'linux' ]
author = PLUGIN_AUTHORS author = PLUGIN_AUTHORS
version = PLUGIN_VERSION_TUPLE version = PLUGIN_VERSION_TUPLE
minimum_calibre_version = (1, 0, 0) minimum_calibre_version = (1, 0, 0)

View File

@@ -34,6 +34,15 @@ from calibre_plugins.obok_dedrm.utilities import (
from calibre_plugins.obok_dedrm.obok.obok import KoboLibrary from calibre_plugins.obok_dedrm.obok.obok import KoboLibrary
from calibre_plugins.obok_dedrm.obok.legacy_obok import legacy_obok from calibre_plugins.obok_dedrm.obok.legacy_obok import legacy_obok
can_parse_xml = True
try:
from xml.etree import ElementTree as ET
debug_print("using xml.etree for xml parsing")
except ImportError:
can_parse_xml = False
debug_print("Cannot find xml.etree, disabling extraction of serial numbers")
PLUGIN_ICONS = ['images/obok.png'] PLUGIN_ICONS = ['images/obok.png']
try: try:
@@ -78,8 +87,42 @@ class InterfacePluginAction(InterfaceAction):
self.current_idx = self.gui.library_view.currentIndex() self.current_idx = self.gui.library_view.currentIndex()
print ('Running {}'.format(PLUGIN_NAME + ' v' + PLUGIN_VERSION)) print ('Running {}'.format(PLUGIN_NAME + ' v' + PLUGIN_VERSION))
#
# search for connected device in case serials are saved
tmpserials = cfg['kobo_serials']
device = self.parent().device_manager.connected_device
device_path = None
if (device):
device_path = device._main_prefix
debug_print("get_device_settings - device_path=", device_path)
# get serial from device_path/.adobe-digital-editions/device.xml
if can_parse_xml:
devicexml = os.path.join(device_path, '.adobe-digital-editions', 'device.xml')
debug_print("trying to load %s" % devicexml)
if (os.path.exists(devicexml)):
debug_print("trying to parse %s" % devicexml)
xmltree = ET.parse(devicexml)
for node in xmltree.iter():
if "deviceSerial" in node.tag:
serial = node.text
debug_print ("found serial %s" % serial)
tmpserials.append(serial)
break
else:
debug_print("didn't find device")
# Get the Kobo Library object (obok v3.01) # Get the Kobo Library object (obok v3.01)
self.library = KoboLibrary() self.library = KoboLibrary(tmpserials, device_path)
debug_print ("got kobodir %s" % self.library.kobodir)
if (self.library.kobodir == ''):
# linux and no device connected, but could be extended
# to the case where on Windows/Mac the prog is not installed
msg = _('<p>Could not find Kobo Library\n<p>Windows/Mac: do you have Kobo Desktop installed?\n<p>Windows/Mac/Linux: In case you have an Kobo eInk device, connect the device.')
showErrorDlg(msg, None)
return
# Get a list of Kobo titles # Get a list of Kobo titles
books = self.build_book_list() books = self.build_book_list()

View File

@@ -3,15 +3,23 @@ from __future__ import (unicode_literals, division, absolute_import,
print_function) print_function)
try: try:
from PyQt5.Qt import (QWidget, QLabel, QVBoxLayout, QHBoxLayout, QComboBox) from PyQt5.Qt import (Qt, QGroupBox, QListWidget, QLineEdit, QDialogButtonBox, QWidget, QLabel, QDialog, QVBoxLayout, QAbstractItemView, QIcon, QHBoxLayout, QComboBox, QListWidgetItem)
except ImportError: except ImportError:
from PyQt4.Qt import (QWidget, QLabel, QVBoxLayout, QHBoxLayout, QComboBox) from PyQt4.Qt import (Qt, QGroupBox, QListWidget, QLineEdit, QDialogButtonBox, QWidget, QLabel, QDialog, QVBoxLayout, QAbstractItemView, QIcon, QHBoxLayout, QComboBox, QListWidgetItem)
try:
from PyQt5 import Qt as QtGui
except ImportError:
from PyQt4 import QtGui
from calibre.gui2 import (error_dialog, question_dialog, info_dialog, open_url)
from calibre.utils.config import JSONConfig, config_dir from calibre.utils.config import JSONConfig, config_dir
plugin_prefs = JSONConfig('plugins/obok_dedrm_prefs') plugin_prefs = JSONConfig('plugins/obok_dedrm_prefs')
plugin_prefs.defaults['finding_homes_for_formats'] = 'Ask' plugin_prefs.defaults['finding_homes_for_formats'] = 'Ask'
plugin_prefs.defaults['kobo_serials'] = []
from calibre_plugins.obok_dedrm.__init__ import PLUGIN_NAME, PLUGIN_VERSION
from calibre_plugins.obok_dedrm.utilities import (debug_print) from calibre_plugins.obok_dedrm.utilities import (debug_print)
try: try:
debug_print("obok::config.py - loading translations") debug_print("obok::config.py - loading translations")
@@ -26,7 +34,10 @@ class ConfigWidget(QWidget):
self.plugin_action = plugin_action self.plugin_action = plugin_action
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
self.setLayout(layout) self.setLayout(layout)
# copy of preferences
self.tmpserials = plugin_prefs['kobo_serials']
combo_label = QLabel(_('When should Obok try to insert EPUBs into existing calibre entries?'), self) combo_label = QLabel(_('When should Obok try to insert EPUBs into existing calibre entries?'), self)
layout.addWidget(combo_label) layout.addWidget(combo_label)
self.find_homes = QComboBox() self.find_homes = QComboBox()
@@ -35,6 +46,178 @@ class ConfigWidget(QWidget):
self.find_homes.addItems([_('Ask'), _('Always'), _('Never')]) self.find_homes.addItems([_('Ask'), _('Always'), _('Never')])
index = self.find_homes.findText(plugin_prefs['finding_homes_for_formats']) index = self.find_homes.findText(plugin_prefs['finding_homes_for_formats'])
self.find_homes.setCurrentIndex(index) self.find_homes.setCurrentIndex(index)
self.serials_button = QtGui.QPushButton(self)
self.serials_button.setToolTip(_(u"Click to manage Kobo serial numbers for Kobo ebooks"))
self.serials_button.setText(u"Kobo devices serials")
self.serials_button.clicked.connect(self.edit_serials)
layout.addWidget(self.serials_button)
def edit_serials(self):
d = ManageKeysDialog(self,u"Kobo device serial numbers",self.tmpserials, AddSerialDialog)
d.exec_()
def save_settings(self): def save_settings(self):
plugin_prefs['finding_homes_for_formats'] = unicode(self.find_homes.currentText()) plugin_prefs['finding_homes_for_formats'] = unicode(self.find_homes.currentText())
plugin_prefs['kobo_serials'] = self.tmpserials
class ManageKeysDialog(QDialog):
def __init__(self, parent, key_type_name, plugin_keys, create_key, keyfile_ext = u""):
QDialog.__init__(self,parent)
self.parent = parent
self.key_type_name = key_type_name
self.plugin_keys = plugin_keys
self.create_key = create_key
self.keyfile_ext = keyfile_ext
self.json_file = (keyfile_ext == u"k4i")
self.setWindowTitle("{0} {1}: Manage {2}s".format(PLUGIN_NAME, PLUGIN_VERSION, self.key_type_name))
# Start Qt Gui dialog layout
layout = QVBoxLayout(self)
self.setLayout(layout)
keys_group_box = QGroupBox(_(u"{0}s".format(self.key_type_name)), self)
layout.addWidget(keys_group_box)
keys_group_box_layout = QHBoxLayout()
keys_group_box.setLayout(keys_group_box_layout)
self.listy = QListWidget(self)
self.listy.setToolTip(u"{0}s that will be used to decrypt ebooks".format(self.key_type_name))
self.listy.setSelectionMode(QAbstractItemView.SingleSelection)
self.populate_list()
keys_group_box_layout.addWidget(self.listy)
button_layout = QVBoxLayout()
keys_group_box_layout.addLayout(button_layout)
self._add_key_button = QtGui.QToolButton(self)
self._add_key_button.setIcon(QIcon(I('plus.png')))
self._add_key_button.setToolTip(u"Create new {0}".format(self.key_type_name))
self._add_key_button.clicked.connect(self.add_key)
button_layout.addWidget(self._add_key_button)
self._delete_key_button = QtGui.QToolButton(self)
self._delete_key_button.setToolTip(_(u"Delete highlighted key"))
self._delete_key_button.setIcon(QIcon(I('list_remove.png')))
self._delete_key_button.clicked.connect(self.delete_key)
button_layout.addWidget(self._delete_key_button)
spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
button_layout.addItem(spacerItem)
layout.addSpacing(5)
migrate_layout = QHBoxLayout()
layout.addLayout(migrate_layout)
migrate_layout.addStretch()
self.button_box = QDialogButtonBox(QDialogButtonBox.Close)
self.button_box.rejected.connect(self.close)
migrate_layout.addWidget(self.button_box)
self.resize(self.sizeHint())
def populate_list(self):
if type(self.plugin_keys) == dict:
for key in self.plugin_keys.keys():
self.listy.addItem(QListWidgetItem(key))
else:
for key in self.plugin_keys:
self.listy.addItem(QListWidgetItem(key))
def add_key(self):
d = self.create_key(self)
d.exec_()
if d.result() != d.Accepted:
# New key generation cancelled.
return
new_key_value = d.key_value
if new_key_value in self.plugin_keys:
info_dialog(None, "{0} {1}: Duplicate {2}".format(PLUGIN_NAME, PLUGIN_VERSION,self.key_type_name),
u"This {0} is already in the list of {0}s has not been added.".format(self.key_type_name), show=True)
return
self.plugin_keys.append(d.key_value)
self.listy.clear()
self.populate_list()
def rename_key(self):
if not self.listy.currentItem():
errmsg = u"No {0} selected to rename. Highlight a keyfile first.".format(self.key_type_name)
r = error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION),
_(errmsg), show=True, show_copy_button=False)
return
d = RenameKeyDialog(self)
d.exec_()
if d.result() != d.Accepted:
# rename cancelled or moot.
return
keyname = unicode(self.listy.currentItem().text())
if not question_dialog(self, "{0} {1}: Confirm Rename".format(PLUGIN_NAME, PLUGIN_VERSION), u"Do you really want to rename the {2} named <strong>{0}</strong> to <strong>{1}</strong>?".format(keyname,d.key_name,self.key_type_name), show_copy_button=False, default_yes=False):
return
self.plugin_keys[d.key_name] = self.plugin_keys[keyname]
del self.plugin_keys[keyname]
self.listy.clear()
self.populate_list()
def delete_key(self):
if not self.listy.currentItem():
return
keyname = unicode(self.listy.currentItem().text())
if not question_dialog(self, "{0} {1}: Confirm Delete".format(PLUGIN_NAME, PLUGIN_VERSION), u"Do you really want to delete the {1} <strong>{0}</strong>?".format(keyname, self.key_type_name), show_copy_button=False, default_yes=False):
return
self.plugin_keys.remove(keyname)
self.listy.clear()
self.populate_list()
class AddSerialDialog(QDialog):
def __init__(self, parent=None,):
QDialog.__init__(self, parent)
self.parent = parent
self.setWindowTitle(u"{0} {1}: Add New eInk Kobo Serial Number".format(PLUGIN_NAME, PLUGIN_VERSION))
layout = QVBoxLayout(self)
self.setLayout(layout)
data_group_box = QGroupBox(u"", self)
layout.addWidget(data_group_box)
data_group_box_layout = QVBoxLayout()
data_group_box.setLayout(data_group_box_layout)
key_group = QHBoxLayout()
data_group_box_layout.addLayout(key_group)
key_group.addWidget(QLabel(u"EInk Kobo Serial Number:", self))
self.key_ledit = QLineEdit("", self)
self.key_ledit.setToolTip(u"Enter an eInk Kobo serial number. EInk Kobo serial numbers are 13 characters long and usually start with a 'N'. Kobo Serial Numbers are case-sensitive, so be sure to enter the upper and lower case letters unchanged.")
key_group.addWidget(self.key_ledit)
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject)
layout.addWidget(self.button_box)
self.resize(self.sizeHint())
@property
def key_name(self):
return unicode(self.key_ledit.text()).strip()
@property
def key_value(self):
return unicode(self.key_ledit.text()).strip()
def accept(self):
if len(self.key_name) == 0 or self.key_name.isspace():
errmsg = u"Please enter an eInk Kindle Serial Number or click Cancel in the dialog."
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
if len(self.key_name) != 13:
errmsg = u"EInk Kobo Serial Numbers must be 13 characters long. This is {0:d} characters long.".format(len(self.key_name))
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
QDialog.accept(self)

View File

@@ -1,6 +1,14 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Version 3.1.6 September 2015
# Enable support for Kobo devices
# More character encoding fixes (unicode strings)
#
# Version 3.1.5 September 2015
# Removed requirement that a purchase has been made.
# Also add in character encoding fixes
#
# Version 3.1.4 September 2015 # Version 3.1.4 September 2015
# Updated for version 3.17 of the Windows Desktop app. # Updated for version 3.17 of the Windows Desktop app.
# #
@@ -115,7 +123,7 @@
# #
"""Manage all Kobo books, either encrypted or DRM-free.""" """Manage all Kobo books, either encrypted or DRM-free."""
__version__ = '3.1.3' __version__ = '3.1.6'
import sys import sys
import os import os
@@ -215,6 +223,24 @@ def _load_crypto():
AES = _load_crypto() AES = _load_crypto()
# Wrap a stream so that output gets flushed immediately
# and also make sure that any unicode strings get
# encoded using "replace" before writing them.
class SafeUnbuffered:
def __init__(self, stream):
self.stream = stream
self.encoding = stream.encoding
if self.encoding == None:
self.encoding = "utf-8"
def write(self, data):
if isinstance(data,unicode):
data = data.encode(self.encoding,"replace")
self.stream.write(data)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
class KoboLibrary(object): class KoboLibrary(object):
"""The Kobo library. """The Kobo library.
@@ -222,23 +248,49 @@ class KoboLibrary(object):
written by the Kobo Desktop Edition application, including the list written by the Kobo Desktop Edition application, including the list
of books, their titles, and the user's encryption key(s).""" of books, their titles, and the user's encryption key(s)."""
def __init__ (self): def __init__ (self, serials = [], device_path = None):
print u"Obok v{0}\nCopyright © 2012-2015 Physisticated et al.".format(__version__) print u"Obok v{0}\nCopyright © 2012-2015 Physisticated et al.".format(__version__)
if sys.platform.startswith('win'): self.kobodir = u""
if sys.getwindowsversion().major > 5: kobodb = u""
self.kobodir = os.environ['LOCALAPPDATA']
else: # - first check whether serials have been found or are provided
self.kobodir = os.path.join(os.environ['USERPROFILE'], 'Local Settings', 'Application Data') # and a device is connected. In this case, use the device
self.kobodir = os.path.join(self.kobodir, 'Kobo', 'Kobo Desktop Edition') # - otherwise fall back to Kobo Desktop Application for Windows and Mac
elif sys.platform.startswith('darwin'): if (device_path and (len(serials) > 0)):
self.kobodir = os.path.join(os.environ['HOME'], 'Library', 'Application Support', 'Kobo', 'Kobo Desktop Edition') self.kobodir = os.path.join(device_path, u".kobo")
self.bookdir = os.path.join(self.kobodir, 'kepub') # devices use KoboReader.sqlite
kobodb = os.path.join(self.kobodir, 'Kobo.sqlite') kobodb = os.path.join(self.kobodir, u"KoboReader.sqlite")
self.__sqlite = sqlite3.connect(kobodb) if (not(os.path.exists(kobodb))):
self.__cursor = self.__sqlite.cursor() # give up here, we haven't found anything useful
self._userkeys = [] self.kobodir = u""
self._books = [] kobodb = u""
self._volumeID = []
if (self.kobodir == u""):
# we haven't found a device with serials, so try desktop apps
if sys.platform.startswith('win'):
import _winreg as winreg
if sys.getwindowsversion().major > 5:
if 'LOCALAPPDATA' in os.environ.keys():
# Python 2.x does not return unicode env. Use Python 3.x
self.kobodir = winreg.ExpandEnvironmentStrings(u"%LOCALAPPDATA%")
if (self.kobodir == u""):
if 'USERPROFILE' in os.environ.keys():
# Python 2.x does not return unicode env. Use Python 3.x
self.kobodir = os.path.join(winreg.ExpandEnvironmentStrings(u"%USERPROFILE%"), u"Local Settings", u"Application Data")
self.kobodir = os.path.join(self.kobodir, u"Kobo", u"Kobo Desktop Edition")
elif sys.platform.startswith('darwin'):
self.kobodir = os.path.join(os.environ['HOME'], u"Library", u"Application Support", u"Kobo", u"Kobo Desktop Edition")
# desktop versions use Kobo.sqlite
kobodb = os.path.join(self.kobodir, u"Kobo.sqlite")
if (self.kobodir != u""):
self.bookdir = os.path.join(self.kobodir, u"kepub")
self.__sqlite = sqlite3.connect(kobodb)
self.__cursor = self.__sqlite.cursor()
self._userkeys = []
self._books = []
self._volumeID = []
self._serials = serials
def close (self): def close (self):
"""Closes the database used by the library.""" """Closes the database used by the library."""
@@ -279,7 +331,7 @@ class KoboLibrary(object):
def __bookfile (self, volumeid): def __bookfile (self, volumeid):
"""The filename needed to open a given book.""" """The filename needed to open a given book."""
return os.path.join(self.kobodir, 'kepub', volumeid) return os.path.join(self.kobodir, u"kepub", volumeid)
def __getmacaddrs (self): def __getmacaddrs (self):
"""The list of all MAC addresses on this machine.""" """The list of all MAC addresses on this machine."""
@@ -295,13 +347,18 @@ class KoboLibrary(object):
output = subprocess.check_output('/sbin/ifconfig -a', shell=True) output = subprocess.check_output('/sbin/ifconfig -a', shell=True)
matches = c.findall(output) matches = c.findall(output)
for m in matches: for m in matches:
# print "m:",m[0] # print u"m:{0}".format(m[0])
macaddrs.append(m[0].upper()) macaddrs.append(m[0].upper())
# extend the list of macaddrs in any case with the serials
# cannot hurt ;-)
macaddrs.extend(self._serials)
return macaddrs return macaddrs
def __getuserids (self): def __getuserids (self):
userids = [] userids = []
cursor = self.__cursor.execute('SELECT UserID FROM user WHERE HasMadePurchase = "true"') cursor = self.__cursor.execute('SELECT UserID FROM user')
row = cursor.fetchone() row = cursor.fetchone()
while row is not None: while row is not None:
try: try:
@@ -432,13 +489,13 @@ class KoboFile(object):
if contents[:5]=="<?xml": if contents[:5]=="<?xml":
return True return True
else: else:
print "Bad XML: ",contents[:5] print u"Bad XML: {0}".format(contents[:5])
raise ValueError raise ValueError
if self.mimetype == 'image/jpeg': if self.mimetype == 'image/jpeg':
if contents[:3] == '\xff\xd8\xff': if contents[:3] == '\xff\xd8\xff':
return True return True
else: else:
print "Bad JPEG: ", contents[:3].encode('hex') print u"Bad JPEG: {0}".format(contents[:3].encode('hex'))
raise ValueError() raise ValueError()
return False return False
@@ -460,36 +517,34 @@ class KoboFile(object):
contents = contents[:-padding] contents = contents[:-padding]
return contents return contents
if __name__ == '__main__': def cli_main():
lib = KoboLibrary() lib = KoboLibrary()
for i, book in enumerate(lib.books): for i, book in enumerate(lib.books):
print ('%d: %s' % (i + 1, book.title)).encode('ascii', 'ignore') print u"{0}: {1}".format(i + 1, book.title)
num_string = raw_input("Convert book number... ") num_string = raw_input(u"Convert book number... ")
try: try:
num = int(num_string) num = int(num_string)
book = lib.books[num - 1] book = lib.books[num - 1]
except (ValueError, IndexError): except (ValueError, IndexError):
exit() exit()
print "Converting", book.title print u"Converting {0}".format(book.title)
zin = zipfile.ZipFile(book.filename, "r") zin = zipfile.ZipFile(book.filename, "r")
# make filename out of Unicode alphanumeric and whitespace equivalents from title # make filename out of Unicode alphanumeric and whitespace equivalents from title
outname = "%s.epub" % (re.sub('[^\s\w]', '', book.title, 0, re.UNICODE)) outname = u"{0}.epub".format(re.sub('[^\s\w]', '_', book.title, 0, re.UNICODE))
if (book.type == 'drm-free'): if (book.type == 'drm-free'):
print "DRM-free book, conversion is not needed" print u"DRM-free book, conversion is not needed"
shutil.copyfile(book.filename, outname) shutil.copyfile(book.filename, outname)
print "Book saved as", os.path.join(os.getcwd(), outname) print u"Book saved as {0}".format(os.path.join(os.getcwd(), outname))
exit(0) exit(0)
result = 1 result = 1
for userkey in lib.userkeys: for userkey in lib.userkeys:
# print "Trying key: ",userkey.encode('hex_codec') print u"Trying key: {0}".format(userkey.encode('hex_codec'))
confirmedGood = False
try: try:
zout = zipfile.ZipFile(outname, "w", zipfile.ZIP_DEFLATED) zout = zipfile.ZipFile(outname, "w", zipfile.ZIP_DEFLATED)
for filename in zin.namelist(): for filename in zin.namelist():
@@ -498,18 +553,25 @@ if __name__ == '__main__':
file = book.encryptedfiles[filename] file = book.encryptedfiles[filename]
contents = file.decrypt(userkey, contents) contents = file.decrypt(userkey, contents)
# Parse failures mean the key is probably wrong. # Parse failures mean the key is probably wrong.
if not confirmedGood: file.check(contents)
confirmedGood = file.check(contents)
zout.writestr(filename, contents) zout.writestr(filename, contents)
zout.close() zout.close()
print "Book saved as", os.path.join(os.getcwd(), outname) print u"Decryption succeeded."
print u"Book saved as {0}".format(os.path.join(os.getcwd(), outname))
result = 0 result = 0
break break
except ValueError: except ValueError:
print "Decryption failed, trying next key" print u"Decryption failed."
zout.close() zout.close()
os.remove(outname) os.remove(outname)
zin.close() zin.close()
lib.close() lib.close()
exit(result) if result != 0:
print u"Could not decrypt book with any of the keys found."
return result
if __name__ == '__main__':
sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
sys.exit(cli_main())

View File

@@ -1,7 +1,7 @@
obok_plugin.zip obok_plugin.zip
================ ================
This plugin will remove the DRM from Kobo ebooks download on Mac or Windows using the Kobo desktop application. This plugin will remove the DRM from Kobo ebooks download on Mac or Windows using the Kobo desktop application, or from Kobo ebooks on an attached Kobo reader. If both are available, ebooks will be read from the attached Kobo reader. To import from the desktop application, unplug the Kobo reader.
Installation Installation
@@ -11,7 +11,7 @@ Do NOT select "Get plugins to enhance calibre" as this is reserved for 'official
Customization Customization
------------- -------------
No customization is required, except choosing which menus will show the plugin. No customization is required, except choosing which menus will show the plugin. Altough the ability to enter a device serial number is given, this should not need to be filled in, as the serial number should be picked up automatically from the attached Kobo reader.
Using the plugin Using the plugin
@@ -47,6 +47,7 @@ Credits
------- -------
The original obok script was by Physisticated The original obok script was by Physisticated
The plugin conversion was done anonymously. The plugin conversion was done anonymously.
The Kobo reader support was added by norbusan
Improvements to the script and the plugin adaption have been by numerous people since. Additional improvements to the script and the plugin adaption by numerous anonymous people.

View File

@@ -1,6 +1,14 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Version 3.1.6 September 2015
# Enable support for Kobo devices
# More character encoding fixes (unicode strings)
#
# Version 3.1.5 September 2015
# Removed requirement that a purchase has been made.
# Also add in character encoding fixes
#
# Version 3.1.4 September 2015 # Version 3.1.4 September 2015
# Updated for version 3.17 of the Windows Desktop app. # Updated for version 3.17 of the Windows Desktop app.
# #
@@ -115,7 +123,7 @@
# #
"""Manage all Kobo books, either encrypted or DRM-free.""" """Manage all Kobo books, either encrypted or DRM-free."""
__version__ = '3.1.3' __version__ = '3.1.6'
import sys import sys
import os import os
@@ -215,6 +223,24 @@ def _load_crypto():
AES = _load_crypto() AES = _load_crypto()
# Wrap a stream so that output gets flushed immediately
# and also make sure that any unicode strings get
# encoded using "replace" before writing them.
class SafeUnbuffered:
def __init__(self, stream):
self.stream = stream
self.encoding = stream.encoding
if self.encoding == None:
self.encoding = "utf-8"
def write(self, data):
if isinstance(data,unicode):
data = data.encode(self.encoding,"replace")
self.stream.write(data)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
class KoboLibrary(object): class KoboLibrary(object):
"""The Kobo library. """The Kobo library.
@@ -222,23 +248,49 @@ class KoboLibrary(object):
written by the Kobo Desktop Edition application, including the list written by the Kobo Desktop Edition application, including the list
of books, their titles, and the user's encryption key(s).""" of books, their titles, and the user's encryption key(s)."""
def __init__ (self): def __init__ (self, serials = [], device_path = None):
print u"Obok v{0}\nCopyright © 2012-2015 Physisticated et al.".format(__version__) print u"Obok v{0}\nCopyright © 2012-2015 Physisticated et al.".format(__version__)
if sys.platform.startswith('win'): self.kobodir = u""
if sys.getwindowsversion().major > 5: kobodb = u""
self.kobodir = os.environ['LOCALAPPDATA']
else: # - first check whether serials have been found or are provided
self.kobodir = os.path.join(os.environ['USERPROFILE'], 'Local Settings', 'Application Data') # and a device is connected. In this case, use the device
self.kobodir = os.path.join(self.kobodir, 'Kobo', 'Kobo Desktop Edition') # - otherwise fall back to Kobo Desktop Application for Windows and Mac
elif sys.platform.startswith('darwin'): if (device_path and (len(serials) > 0)):
self.kobodir = os.path.join(os.environ['HOME'], 'Library', 'Application Support', 'Kobo', 'Kobo Desktop Edition') self.kobodir = os.path.join(device_path, u".kobo")
self.bookdir = os.path.join(self.kobodir, 'kepub') # devices use KoboReader.sqlite
kobodb = os.path.join(self.kobodir, 'Kobo.sqlite') kobodb = os.path.join(self.kobodir, u"KoboReader.sqlite")
self.__sqlite = sqlite3.connect(kobodb) if (not(os.path.exists(kobodb))):
self.__cursor = self.__sqlite.cursor() # give up here, we haven't found anything useful
self._userkeys = [] self.kobodir = u""
self._books = [] kobodb = u""
self._volumeID = []
if (self.kobodir == u""):
# we haven't found a device with serials, so try desktop apps
if sys.platform.startswith('win'):
import _winreg as winreg
if sys.getwindowsversion().major > 5:
if 'LOCALAPPDATA' in os.environ.keys():
# Python 2.x does not return unicode env. Use Python 3.x
self.kobodir = winreg.ExpandEnvironmentStrings(u"%LOCALAPPDATA%")
if (self.kobodir == u""):
if 'USERPROFILE' in os.environ.keys():
# Python 2.x does not return unicode env. Use Python 3.x
self.kobodir = os.path.join(winreg.ExpandEnvironmentStrings(u"%USERPROFILE%"), u"Local Settings", u"Application Data")
self.kobodir = os.path.join(self.kobodir, u"Kobo", u"Kobo Desktop Edition")
elif sys.platform.startswith('darwin'):
self.kobodir = os.path.join(os.environ['HOME'], u"Library", u"Application Support", u"Kobo", u"Kobo Desktop Edition")
# desktop versions use Kobo.sqlite
kobodb = os.path.join(self.kobodir, u"Kobo.sqlite")
if (self.kobodir != u""):
self.bookdir = os.path.join(self.kobodir, u"kepub")
self.__sqlite = sqlite3.connect(kobodb)
self.__cursor = self.__sqlite.cursor()
self._userkeys = []
self._books = []
self._volumeID = []
self._serials = serials
def close (self): def close (self):
"""Closes the database used by the library.""" """Closes the database used by the library."""
@@ -279,7 +331,7 @@ class KoboLibrary(object):
def __bookfile (self, volumeid): def __bookfile (self, volumeid):
"""The filename needed to open a given book.""" """The filename needed to open a given book."""
return os.path.join(self.kobodir, 'kepub', volumeid) return os.path.join(self.kobodir, u"kepub", volumeid)
def __getmacaddrs (self): def __getmacaddrs (self):
"""The list of all MAC addresses on this machine.""" """The list of all MAC addresses on this machine."""
@@ -295,13 +347,18 @@ class KoboLibrary(object):
output = subprocess.check_output('/sbin/ifconfig -a', shell=True) output = subprocess.check_output('/sbin/ifconfig -a', shell=True)
matches = c.findall(output) matches = c.findall(output)
for m in matches: for m in matches:
# print "m:",m[0] # print u"m:{0}".format(m[0])
macaddrs.append(m[0].upper()) macaddrs.append(m[0].upper())
# extend the list of macaddrs in any case with the serials
# cannot hurt ;-)
macaddrs.extend(self._serials)
return macaddrs return macaddrs
def __getuserids (self): def __getuserids (self):
userids = [] userids = []
cursor = self.__cursor.execute('SELECT UserID FROM user WHERE HasMadePurchase = "true"') cursor = self.__cursor.execute('SELECT UserID FROM user')
row = cursor.fetchone() row = cursor.fetchone()
while row is not None: while row is not None:
try: try:
@@ -432,13 +489,13 @@ class KoboFile(object):
if contents[:5]=="<?xml": if contents[:5]=="<?xml":
return True return True
else: else:
print "Bad XML: ",contents[:5] print u"Bad XML: {0}".format(contents[:5])
raise ValueError raise ValueError
if self.mimetype == 'image/jpeg': if self.mimetype == 'image/jpeg':
if contents[:3] == '\xff\xd8\xff': if contents[:3] == '\xff\xd8\xff':
return True return True
else: else:
print "Bad JPEG: ", contents[:3].encode('hex') print u"Bad JPEG: {0}".format(contents[:3].encode('hex'))
raise ValueError() raise ValueError()
return False return False
@@ -460,36 +517,34 @@ class KoboFile(object):
contents = contents[:-padding] contents = contents[:-padding]
return contents return contents
if __name__ == '__main__': def cli_main():
lib = KoboLibrary() lib = KoboLibrary()
for i, book in enumerate(lib.books): for i, book in enumerate(lib.books):
print ('%d: %s' % (i + 1, book.title)).encode('ascii', 'ignore') print u"{0}: {1}".format(i + 1, book.title)
num_string = raw_input("Convert book number... ") num_string = raw_input(u"Convert book number... ")
try: try:
num = int(num_string) num = int(num_string)
book = lib.books[num - 1] book = lib.books[num - 1]
except (ValueError, IndexError): except (ValueError, IndexError):
exit() exit()
print "Converting", book.title print u"Converting {0}".format(book.title)
zin = zipfile.ZipFile(book.filename, "r") zin = zipfile.ZipFile(book.filename, "r")
# make filename out of Unicode alphanumeric and whitespace equivalents from title # make filename out of Unicode alphanumeric and whitespace equivalents from title
outname = "%s.epub" % (re.sub('[^\s\w]', '', book.title, 0, re.UNICODE)) outname = u"{0}.epub".format(re.sub('[^\s\w]', '_', book.title, 0, re.UNICODE))
if (book.type == 'drm-free'): if (book.type == 'drm-free'):
print "DRM-free book, conversion is not needed" print u"DRM-free book, conversion is not needed"
shutil.copyfile(book.filename, outname) shutil.copyfile(book.filename, outname)
print "Book saved as", os.path.join(os.getcwd(), outname) print u"Book saved as {0}".format(os.path.join(os.getcwd(), outname))
exit(0) exit(0)
result = 1 result = 1
for userkey in lib.userkeys: for userkey in lib.userkeys:
# print "Trying key: ",userkey.encode('hex_codec') print u"Trying key: {0}".format(userkey.encode('hex_codec'))
confirmedGood = False
try: try:
zout = zipfile.ZipFile(outname, "w", zipfile.ZIP_DEFLATED) zout = zipfile.ZipFile(outname, "w", zipfile.ZIP_DEFLATED)
for filename in zin.namelist(): for filename in zin.namelist():
@@ -498,18 +553,25 @@ if __name__ == '__main__':
file = book.encryptedfiles[filename] file = book.encryptedfiles[filename]
contents = file.decrypt(userkey, contents) contents = file.decrypt(userkey, contents)
# Parse failures mean the key is probably wrong. # Parse failures mean the key is probably wrong.
if not confirmedGood: file.check(contents)
confirmedGood = file.check(contents)
zout.writestr(filename, contents) zout.writestr(filename, contents)
zout.close() zout.close()
print "Book saved as", os.path.join(os.getcwd(), outname) print u"Decryption succeeded."
print u"Book saved as {0}".format(os.path.join(os.getcwd(), outname))
result = 0 result = 0
break break
except ValueError: except ValueError:
print "Decryption failed, trying next key" print u"Decryption failed."
zout.close() zout.close()
os.remove(outname) os.remove(outname)
zin.close() zin.close()
lib.close() lib.close()
exit(result) if result != 0:
print u"Could not decrypt book with any of the keys found."
return result
if __name__ == '__main__':
sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
sys.exit(cli_main())