Compare commits

...

18 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
apprenticeharper
07485be2c0 Add transparency to the obok logo 2015-09-03 08:06:33 +01:00
apprenticeharper
e5e269fbae Fixes for android key extraction 2015-09-03 07:51:10 +01:00
apprenticeharper
d54dc38c2d Fix for Kobo Desktop 3.17 2015-09-03 07:49:09 +01:00
apprenticeharper
3c322f3695 Second and last step to fix capitalisation issue 2015-09-01 07:08:46 +01:00
apprenticeharper
c112c28f58 First step to fix capitalisation issue 2015-09-01 07:06:13 +01:00
apprenticeharper
b8606cd182 Merge pull request #32 from eli-schwartz/master
Linux: Allow using ~ when specifying a wineprefix.
2015-09-01 06:57:48 +01:00
Eli Schwartz
f2190a6755 Linux: Allow using ~ when specifying a wineprefix. 2015-08-12 17:39:50 -04:00
apprenticeharper
33d8a63f61 Fix for plugin bug introduced in 6.3.1 for Kindle for PC 2015-08-12 18:35:21 +01:00
20 changed files with 606 additions and 154 deletions

View File

@@ -24,7 +24,7 @@
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>droplet</string> <string>droplet</string>
<key>CFBundleGetInfoString</key> <key>CFBundleGetInfoString</key>
<string>DeDRM AppleScript 6.3.2 Written 20102015 by Apprentice Alf et al.</string> <string>DeDRM AppleScript 6.3.4 Written 20102015 by Apprentice Alf et al.</string>
<key>CFBundleIconFile</key> <key>CFBundleIconFile</key>
<string>DeDRM</string> <string>DeDRM</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
@@ -36,7 +36,7 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>6.3.2</string> <string>6.3.4</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>dplt</string> <string>dplt</string>
<key>LSRequiresCarbon</key> <key>LSRequiresCarbon</key>

View File

@@ -44,6 +44,8 @@ __docformat__ = 'restructuredtext en'
# 6.3.0 - Added in Kindle for Android serial number solution # 6.3.0 - Added in Kindle for Android serial number solution
# 6.3.1 - Version number bump for clarity # 6.3.1 - Version number bump for clarity
# 6.3.2 - Fixed Kindle for Android help file # 6.3.2 - Fixed Kindle for Android help file
# 6.3.3 - Bug fix for Kindle for PC support
# 6.3.4 - Fixes for Kindle for Android, Linux, and Kobo 3.17
""" """
@@ -51,7 +53,7 @@ Decrypt DRMed ebooks.
""" """
PLUGIN_NAME = u"DeDRM" PLUGIN_NAME = u"DeDRM"
PLUGIN_VERSION_TUPLE = (6, 3, 2) PLUGIN_VERSION_TUPLE = (6, 3, 4)
PLUGIN_VERSION = u".".join([unicode(str(x)) for x in PLUGIN_VERSION_TUPLE]) PLUGIN_VERSION = u".".join([unicode(str(x)) for x in PLUGIN_VERSION_TUPLE])
# Include an html helpfile in the plugin's zipfile with the following name. # Include an html helpfile in the plugin's zipfile with the following name.
RESOURCE_NAME = PLUGIN_NAME + '_Help.htm' RESOURCE_NAME = PLUGIN_NAME + '_Help.htm'
@@ -147,7 +149,7 @@ class DeDRM(FileTypePlugin):
try: try:
open(file_path,'wb').write(data) open(file_path,'wb').write(data)
except: except:
print u"{0} v{1}: Exception when copying needed library files after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) print u"{0} v{1}: Exception when copying needed library files".format(PLUGIN_NAME, PLUGIN_VERSION)
traceback.print_exc() traceback.print_exc()
pass pass
@@ -523,7 +525,7 @@ class DeDRM(FileTypePlugin):
if len(newkeys) > 0: if len(newkeys) > 0:
print u"{0} v{1}: Found {2} new {3}".format(PLUGIN_NAME, PLUGIN_VERSION, len(newkeys), u"key" if len(newkeys)==1 else u"keys") print u"{0} v{1}: Found {2} new {3}".format(PLUGIN_NAME, PLUGIN_VERSION, len(newkeys), u"key" if len(newkeys)==1 else u"keys")
try: try:
book = k4mobidedrm.GetDecryptedBook(path_to_ebook,newkeys.items(),[],[],self.starttime) book = k4mobidedrm.GetDecryptedBook(path_to_ebook,newkeys.items(),[],[],[],self.starttime)
decoded = True decoded = True
# store the new successful keys in the defaults # store the new successful keys in the defaults
print u"{0} v{1}: Saving {2} new {3}".format(PLUGIN_NAME, PLUGIN_VERSION, len(newkeys), u"key" if len(newkeys)==1 else u"keys") print u"{0} v{1}: Saving {2} new {3}".format(PLUGIN_NAME, PLUGIN_VERSION, len(newkeys), u"key" if len(newkeys)==1 else u"keys")

View File

@@ -16,16 +16,18 @@ from __future__ import with_statement
# - and added in unicode command line support # - and added in unicode command line support
# 1.3 - added in TkInter interface, output to a file # 1.3 - added in TkInter interface, output to a file
# 1.4 - Fix some problems identified by Aldo Bleeker # 1.4 - Fix some problems identified by Aldo Bleeker
# 1.5 - Fix another problem identified by Aldo Bleeker
""" """
Retrieve Kindle for Android Serial Number. Retrieve Kindle for Android Serial Number.
""" """
__license__ = 'GPL v3' __license__ = 'GPL v3'
__version__ = '1.4' __version__ = '1.5'
import os import os
import sys import sys
import traceback
import getopt import getopt
import tempfile import tempfile
import zlib import zlib
@@ -220,20 +222,30 @@ def get_serials2(path=STORAGE2):
userdata_keys = cursor.fetchall() userdata_keys = cursor.fetchall()
dsns = [] dsns = []
for userdata_row in userdata_keys: for userdata_row in userdata_keys:
if userdata_row: try:
userdata_utf8 = userdata_row[0].encode('utf8') if userdata_row and userdata_row[0]:
if len(userdata_utf8) > 0: userdata_utf8 = userdata_row[0].encode('utf8')
dsns.append(userdata_utf8) if len(userdata_utf8) > 0:
dsns.append(userdata_utf8)
except:
print "Error getting one of the device serial name keys"
traceback.print_exc()
pass
dsns = list(set(dsns)) dsns = list(set(dsns))
cursor.execute('''select userdata_value from userdata where userdata_key like '%/%kindle.account.tokens%' ''') cursor.execute('''select userdata_value from userdata where userdata_key like '%/%kindle.account.tokens%' ''')
userdata_keys = cursor.fetchall() userdata_keys = cursor.fetchall()
tokens = [] tokens = []
for userdata_row in userdata_keys: for userdata_row in userdata_keys:
if userdata_row: try:
userdata_utf8 = userdata_row[0].encode('utf8') if userdata_row and userdata_row[0]:
if len(userdata_utf8) > 0: userdata_utf8 = userdata_row[0].encode('utf8')
tokens.append(userdata_utf8) if len(userdata_utf8) > 0:
tokens.append(userdata_utf8)
except:
print "Error getting one of the account token keys"
traceback.print_exc()
pass
tokens = list(set(tokens)) tokens = list(set(tokens))
serials = [] serials = []
@@ -377,7 +389,6 @@ def gui_main():
import Tkconstants import Tkconstants
import tkMessageBox import tkMessageBox
import tkFileDialog import tkFileDialog
import traceback
except: except:
print "Tkinter not installed" print "Tkinter not installed"
return cli_main() return cli_main()

View File

@@ -17,8 +17,10 @@
# 6.3.0 - Add in Android support # 6.3.0 - Add in Android support
# 6.3.1 - Version bump for clarity # 6.3.1 - Version bump for clarity
# 6.3.2 - Version bump to match plugin # 6.3.2 - Version bump to match plugin
# 6.3.3 - Version bump to match plugin
# 6.3.4 - Version bump to match plugin
__version__ = '6.3.2' __version__ = '6.3.4'
import sys import sys
import os, os.path import os, os.path

View File

@@ -44,6 +44,8 @@ __docformat__ = 'restructuredtext en'
# 6.3.0 - Added in Kindle for Android serial number solution # 6.3.0 - Added in Kindle for Android serial number solution
# 6.3.1 - Version number bump for clarity # 6.3.1 - Version number bump for clarity
# 6.3.2 - Fixed Kindle for Android help file # 6.3.2 - Fixed Kindle for Android help file
# 6.3.3 - Bug fix for Kindle for PC support
# 6.3.4 - Fixes for Kindle for Android, Linux, and Kobo 3.17
""" """
@@ -51,7 +53,7 @@ Decrypt DRMed ebooks.
""" """
PLUGIN_NAME = u"DeDRM" PLUGIN_NAME = u"DeDRM"
PLUGIN_VERSION_TUPLE = (6, 3, 2) PLUGIN_VERSION_TUPLE = (6, 3, 4)
PLUGIN_VERSION = u".".join([unicode(str(x)) for x in PLUGIN_VERSION_TUPLE]) PLUGIN_VERSION = u".".join([unicode(str(x)) for x in PLUGIN_VERSION_TUPLE])
# Include an html helpfile in the plugin's zipfile with the following name. # Include an html helpfile in the plugin's zipfile with the following name.
RESOURCE_NAME = PLUGIN_NAME + '_Help.htm' RESOURCE_NAME = PLUGIN_NAME + '_Help.htm'
@@ -147,7 +149,7 @@ class DeDRM(FileTypePlugin):
try: try:
open(file_path,'wb').write(data) open(file_path,'wb').write(data)
except: except:
print u"{0} v{1}: Exception when copying needed library files after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) print u"{0} v{1}: Exception when copying needed library files".format(PLUGIN_NAME, PLUGIN_VERSION)
traceback.print_exc() traceback.print_exc()
pass pass
@@ -523,7 +525,7 @@ class DeDRM(FileTypePlugin):
if len(newkeys) > 0: if len(newkeys) > 0:
print u"{0} v{1}: Found {2} new {3}".format(PLUGIN_NAME, PLUGIN_VERSION, len(newkeys), u"key" if len(newkeys)==1 else u"keys") print u"{0} v{1}: Found {2} new {3}".format(PLUGIN_NAME, PLUGIN_VERSION, len(newkeys), u"key" if len(newkeys)==1 else u"keys")
try: try:
book = k4mobidedrm.GetDecryptedBook(path_to_ebook,newkeys.items(),[],[],self.starttime) book = k4mobidedrm.GetDecryptedBook(path_to_ebook,newkeys.items(),[],[],[],self.starttime)
decoded = True decoded = True
# store the new successful keys in the defaults # store the new successful keys in the defaults
print u"{0} v{1}: Saving {2} new {3}".format(PLUGIN_NAME, PLUGIN_VERSION, len(newkeys), u"key" if len(newkeys)==1 else u"keys") print u"{0} v{1}: Saving {2} new {3}".format(PLUGIN_NAME, PLUGIN_VERSION, len(newkeys), u"key" if len(newkeys)==1 else u"keys")

View File

@@ -16,16 +16,18 @@ from __future__ import with_statement
# - and added in unicode command line support # - and added in unicode command line support
# 1.3 - added in TkInter interface, output to a file # 1.3 - added in TkInter interface, output to a file
# 1.4 - Fix some problems identified by Aldo Bleeker # 1.4 - Fix some problems identified by Aldo Bleeker
# 1.5 - Fix another problem identified by Aldo Bleeker
""" """
Retrieve Kindle for Android Serial Number. Retrieve Kindle for Android Serial Number.
""" """
__license__ = 'GPL v3' __license__ = 'GPL v3'
__version__ = '1.4' __version__ = '1.5'
import os import os
import sys import sys
import traceback
import getopt import getopt
import tempfile import tempfile
import zlib import zlib
@@ -220,20 +222,30 @@ def get_serials2(path=STORAGE2):
userdata_keys = cursor.fetchall() userdata_keys = cursor.fetchall()
dsns = [] dsns = []
for userdata_row in userdata_keys: for userdata_row in userdata_keys:
if userdata_row: try:
userdata_utf8 = userdata_row[0].encode('utf8') if userdata_row and userdata_row[0]:
if len(userdata_utf8) > 0: userdata_utf8 = userdata_row[0].encode('utf8')
dsns.append(userdata_utf8) if len(userdata_utf8) > 0:
dsns.append(userdata_utf8)
except:
print "Error getting one of the device serial name keys"
traceback.print_exc()
pass
dsns = list(set(dsns)) dsns = list(set(dsns))
cursor.execute('''select userdata_value from userdata where userdata_key like '%/%kindle.account.tokens%' ''') cursor.execute('''select userdata_value from userdata where userdata_key like '%/%kindle.account.tokens%' ''')
userdata_keys = cursor.fetchall() userdata_keys = cursor.fetchall()
tokens = [] tokens = []
for userdata_row in userdata_keys: for userdata_row in userdata_keys:
if userdata_row: try:
userdata_utf8 = userdata_row[0].encode('utf8') if userdata_row and userdata_row[0]:
if len(userdata_utf8) > 0: userdata_utf8 = userdata_row[0].encode('utf8')
tokens.append(userdata_utf8) if len(userdata_utf8) > 0:
tokens.append(userdata_utf8)
except:
print "Error getting one of the account token keys"
traceback.print_exc()
pass
tokens = list(set(tokens)) tokens = list(set(tokens))
serials = [] serials = []
@@ -377,7 +389,6 @@ def gui_main():
import Tkconstants import Tkconstants
import tkMessageBox import tkMessageBox
import tkFileDialog import tkFileDialog
import traceback
except: except:
print "Tkinter not installed" print "Tkinter not installed"
return cli_main() return cli_main()

View File

@@ -44,6 +44,8 @@ __docformat__ = 'restructuredtext en'
# 6.3.0 - Added in Kindle for Android serial number solution # 6.3.0 - Added in Kindle for Android serial number solution
# 6.3.1 - Version number bump for clarity # 6.3.1 - Version number bump for clarity
# 6.3.2 - Fixed Kindle for Android help file # 6.3.2 - Fixed Kindle for Android help file
# 6.3.3 - Bug fix for Kindle for PC support
# 6.3.4 - Fixes for Kindle for Android, Linux, and Kobo 3.17
""" """
@@ -51,7 +53,7 @@ Decrypt DRMed ebooks.
""" """
PLUGIN_NAME = u"DeDRM" PLUGIN_NAME = u"DeDRM"
PLUGIN_VERSION_TUPLE = (6, 3, 2) PLUGIN_VERSION_TUPLE = (6, 3, 4)
PLUGIN_VERSION = u".".join([unicode(str(x)) for x in PLUGIN_VERSION_TUPLE]) PLUGIN_VERSION = u".".join([unicode(str(x)) for x in PLUGIN_VERSION_TUPLE])
# Include an html helpfile in the plugin's zipfile with the following name. # Include an html helpfile in the plugin's zipfile with the following name.
RESOURCE_NAME = PLUGIN_NAME + '_Help.htm' RESOURCE_NAME = PLUGIN_NAME + '_Help.htm'
@@ -147,7 +149,7 @@ class DeDRM(FileTypePlugin):
try: try:
open(file_path,'wb').write(data) open(file_path,'wb').write(data)
except: except:
print u"{0} v{1}: Exception when copying needed library files after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) print u"{0} v{1}: Exception when copying needed library files".format(PLUGIN_NAME, PLUGIN_VERSION)
traceback.print_exc() traceback.print_exc()
pass pass
@@ -523,7 +525,7 @@ class DeDRM(FileTypePlugin):
if len(newkeys) > 0: if len(newkeys) > 0:
print u"{0} v{1}: Found {2} new {3}".format(PLUGIN_NAME, PLUGIN_VERSION, len(newkeys), u"key" if len(newkeys)==1 else u"keys") print u"{0} v{1}: Found {2} new {3}".format(PLUGIN_NAME, PLUGIN_VERSION, len(newkeys), u"key" if len(newkeys)==1 else u"keys")
try: try:
book = k4mobidedrm.GetDecryptedBook(path_to_ebook,newkeys.items(),[],[],self.starttime) book = k4mobidedrm.GetDecryptedBook(path_to_ebook,newkeys.items(),[],[],[],self.starttime)
decoded = True decoded = True
# store the new successful keys in the defaults # store the new successful keys in the defaults
print u"{0} v{1}: Saving {2} new {3}".format(PLUGIN_NAME, PLUGIN_VERSION, len(newkeys), u"key" if len(newkeys)==1 else u"keys") print u"{0} v{1}: Saving {2} new {3}".format(PLUGIN_NAME, PLUGIN_VERSION, len(newkeys), u"key" if len(newkeys)==1 else u"keys")

View File

@@ -16,16 +16,18 @@ from __future__ import with_statement
# - and added in unicode command line support # - and added in unicode command line support
# 1.3 - added in TkInter interface, output to a file # 1.3 - added in TkInter interface, output to a file
# 1.4 - Fix some problems identified by Aldo Bleeker # 1.4 - Fix some problems identified by Aldo Bleeker
# 1.5 - Fix another problem identified by Aldo Bleeker
""" """
Retrieve Kindle for Android Serial Number. Retrieve Kindle for Android Serial Number.
""" """
__license__ = 'GPL v3' __license__ = 'GPL v3'
__version__ = '1.4' __version__ = '1.5'
import os import os
import sys import sys
import traceback
import getopt import getopt
import tempfile import tempfile
import zlib import zlib
@@ -220,20 +222,30 @@ def get_serials2(path=STORAGE2):
userdata_keys = cursor.fetchall() userdata_keys = cursor.fetchall()
dsns = [] dsns = []
for userdata_row in userdata_keys: for userdata_row in userdata_keys:
if userdata_row: try:
userdata_utf8 = userdata_row[0].encode('utf8') if userdata_row and userdata_row[0]:
if len(userdata_utf8) > 0: userdata_utf8 = userdata_row[0].encode('utf8')
dsns.append(userdata_utf8) if len(userdata_utf8) > 0:
dsns.append(userdata_utf8)
except:
print "Error getting one of the device serial name keys"
traceback.print_exc()
pass
dsns = list(set(dsns)) dsns = list(set(dsns))
cursor.execute('''select userdata_value from userdata where userdata_key like '%/%kindle.account.tokens%' ''') cursor.execute('''select userdata_value from userdata where userdata_key like '%/%kindle.account.tokens%' ''')
userdata_keys = cursor.fetchall() userdata_keys = cursor.fetchall()
tokens = [] tokens = []
for userdata_row in userdata_keys: for userdata_row in userdata_keys:
if userdata_row: try:
userdata_utf8 = userdata_row[0].encode('utf8') if userdata_row and userdata_row[0]:
if len(userdata_utf8) > 0: userdata_utf8 = userdata_row[0].encode('utf8')
tokens.append(userdata_utf8) if len(userdata_utf8) > 0:
tokens.append(userdata_utf8)
except:
print "Error getting one of the account token keys"
traceback.print_exc()
pass
tokens = list(set(tokens)) tokens = list(set(tokens))
serials = [] serials = []
@@ -377,7 +389,6 @@ def gui_main():
import Tkconstants import Tkconstants
import tkMessageBox import tkMessageBox
import tkFileDialog import tkFileDialog
import traceback
except: except:
print "Tkinter not installed" print "Tkinter not installed"
return cli_main() return cli_main()

View File

@@ -26,6 +26,7 @@ def WineGetKeys(scriptpath, extension, wineprefix=""):
if not os.path.exists(outdirpath): if not os.path.exists(outdirpath):
os.makedirs(outdirpath) os.makedirs(outdirpath)
wineprefix = os.path.abspath(os.path.expanduser(os.path.expandvars(wineprefix)))
if wineprefix != "" and os.path.exists(wineprefix): if wineprefix != "" and os.path.exists(wineprefix):
cmdline = u"WINEPREFIX=\"{2}\" wine python.exe \"{0}\" \"{1}\"".format(scriptpath,outdirpath,wineprefix) cmdline = u"WINEPREFIX=\"{2}\" wine python.exe \"{0}\" \"{1}\"".format(scriptpath,outdirpath,wineprefix)
else: else:

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, 3) 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

@@ -6,7 +6,7 @@ __license__ = 'GPL v3'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import os, zipfile import os, traceback, zipfile
try: try:
from PyQt5.Qt import QToolButton, QUrl from PyQt5.Qt import QToolButton, QUrl
@@ -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()
@@ -98,6 +141,7 @@ class InterfacePluginAction(InterfaceAction):
candidate_keys = self.library.userkeys candidate_keys = self.library.userkeys
except: except:
print (_('Trouble retrieving keys with newer obok method.')) print (_('Trouble retrieving keys with newer obok method.'))
traceback.print_exc()
else: else:
if len(candidate_keys): if len(candidate_keys):
self.userkeys.extend(candidate_keys) self.userkeys.extend(candidate_keys)

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")
@@ -27,6 +35,9 @@ class ConfigWidget(QWidget):
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()
@@ -36,5 +47,177 @@ class ConfigWidget(QWidget):
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)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -1,6 +1,17 @@
#!/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
# Updated for version 3.17 of the Windows Desktop app.
#
# Version 3.1.3 August 2015 # Version 3.1.3 August 2015
# Add translations for Portuguese and Arabic # Add translations for Portuguese and Arabic
# #
@@ -112,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
@@ -212,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.
@@ -219,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-2014 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."""
@@ -249,9 +304,8 @@ class KoboLibrary(object):
""" """
if len(self._userkeys) != 0: if len(self._userkeys) != 0:
return self._userkeys return self._userkeys
userid = self.__getuserid()
for macaddr in self.__getmacaddrs(): for macaddr in self.__getmacaddrs():
self._userkeys.append(self.__getuserkey(macaddr, userid)) self._userkeys.extend(self.__getuserkeys(macaddr))
return self._userkeys return self._userkeys
@property @property
@@ -277,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."""
@@ -293,17 +347,42 @@ 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 __getuserid (self): def __getuserids (self):
return self.__cursor.execute('SELECT UserID FROM user WHERE HasMadePurchase = "true"').fetchone()[0] userids = []
cursor = self.__cursor.execute('SELECT UserID FROM user')
row = cursor.fetchone()
while row is not None:
try:
userid = row[0]
userids.append(userid)
except:
pass
row = cursor.fetchone()
return userids
def __getuserkey (self, macaddr, userid): def __getuserkeys (self, macaddr):
userids = self.__getuserids()
userkeys = []
# This version is used for versions before 3.17.0.
deviceid = hashlib.sha256('NoCanLook' + macaddr).hexdigest() deviceid = hashlib.sha256('NoCanLook' + macaddr).hexdigest()
userkey = hashlib.sha256(deviceid + userid).hexdigest() for userid in userids:
return binascii.a2b_hex(userkey[32:]) userkey = hashlib.sha256(deviceid + userid).hexdigest()
userkeys.append(binascii.a2b_hex(userkey[32:]))
# This version is used for 3.17.0 and later.
deviceid = hashlib.sha256('XzUhGYdFp' + macaddr).hexdigest()
for userid in userids:
userkey = hashlib.sha256(deviceid + userid).hexdigest()
userkeys.append(binascii.a2b_hex(userkey[32:]))
return userkeys
class KoboBook(object): class KoboBook(object):
"""A Kobo book. """A Kobo book.
@@ -410,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
@@ -438,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():
@@ -476,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

@@ -16,16 +16,18 @@ from __future__ import with_statement
# - and added in unicode command line support # - and added in unicode command line support
# 1.3 - added in TkInter interface, output to a file # 1.3 - added in TkInter interface, output to a file
# 1.4 - Fix some problems identified by Aldo Bleeker # 1.4 - Fix some problems identified by Aldo Bleeker
# 1.5 - Fix another problem identified by Aldo Bleeker
""" """
Retrieve Kindle for Android Serial Number. Retrieve Kindle for Android Serial Number.
""" """
__license__ = 'GPL v3' __license__ = 'GPL v3'
__version__ = '1.4' __version__ = '1.5'
import os import os
import sys import sys
import traceback
import getopt import getopt
import tempfile import tempfile
import zlib import zlib
@@ -220,20 +222,30 @@ def get_serials2(path=STORAGE2):
userdata_keys = cursor.fetchall() userdata_keys = cursor.fetchall()
dsns = [] dsns = []
for userdata_row in userdata_keys: for userdata_row in userdata_keys:
if userdata_row: try:
userdata_utf8 = userdata_row[0].encode('utf8') if userdata_row and userdata_row[0]:
if len(userdata_utf8) > 0: userdata_utf8 = userdata_row[0].encode('utf8')
dsns.append(userdata_utf8) if len(userdata_utf8) > 0:
dsns.append(userdata_utf8)
except:
print "Error getting one of the device serial name keys"
traceback.print_exc()
pass
dsns = list(set(dsns)) dsns = list(set(dsns))
cursor.execute('''select userdata_value from userdata where userdata_key like '%/%kindle.account.tokens%' ''') cursor.execute('''select userdata_value from userdata where userdata_key like '%/%kindle.account.tokens%' ''')
userdata_keys = cursor.fetchall() userdata_keys = cursor.fetchall()
tokens = [] tokens = []
for userdata_row in userdata_keys: for userdata_row in userdata_keys:
if userdata_row: try:
userdata_utf8 = userdata_row[0].encode('utf8') if userdata_row and userdata_row[0]:
if len(userdata_utf8) > 0: userdata_utf8 = userdata_row[0].encode('utf8')
tokens.append(userdata_utf8) if len(userdata_utf8) > 0:
tokens.append(userdata_utf8)
except:
print "Error getting one of the account token keys"
traceback.print_exc()
pass
tokens = list(set(tokens)) tokens = list(set(tokens))
serials = [] serials = []
@@ -377,7 +389,6 @@ def gui_main():
import Tkconstants import Tkconstants
import tkMessageBox import tkMessageBox
import tkFileDialog import tkFileDialog
import traceback
except: except:
print "Tkinter not installed" print "Tkinter not installed"
return cli_main() return cli_main()

View File

@@ -1,6 +1,20 @@
#!/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
# Updated for version 3.17 of the Windows Desktop app.
#
# Version 3.1.3 August 2015
# Add translations for Portuguese and Arabic
#
# Version 3.1.2 January 2015 # Version 3.1.2 January 2015
# Add coding, version number and version announcement # Add coding, version number and version announcement
# #
@@ -109,7 +123,7 @@
# #
"""Manage all Kobo books, either encrypted or DRM-free.""" """Manage all Kobo books, either encrypted or DRM-free."""
__version__ = '3.1.1' __version__ = '3.1.6'
import sys import sys
import os import os
@@ -209,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.
@@ -216,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-2014 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."""
@@ -246,9 +304,8 @@ class KoboLibrary(object):
""" """
if len(self._userkeys) != 0: if len(self._userkeys) != 0:
return self._userkeys return self._userkeys
userid = self.__getuserid()
for macaddr in self.__getmacaddrs(): for macaddr in self.__getmacaddrs():
self._userkeys.append(self.__getuserkey(macaddr, userid)) self._userkeys.extend(self.__getuserkeys(macaddr))
return self._userkeys return self._userkeys
@property @property
@@ -274,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."""
@@ -290,17 +347,42 @@ 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 __getuserid (self): def __getuserids (self):
return self.__cursor.execute('SELECT UserID FROM user WHERE HasMadePurchase = "true"').fetchone()[0] userids = []
cursor = self.__cursor.execute('SELECT UserID FROM user')
row = cursor.fetchone()
while row is not None:
try:
userid = row[0]
userids.append(userid)
except:
pass
row = cursor.fetchone()
return userids
def __getuserkey (self, macaddr, userid): def __getuserkeys (self, macaddr):
userids = self.__getuserids()
userkeys = []
# This version is used for versions before 3.17.0.
deviceid = hashlib.sha256('NoCanLook' + macaddr).hexdigest() deviceid = hashlib.sha256('NoCanLook' + macaddr).hexdigest()
userkey = hashlib.sha256(deviceid + userid).hexdigest() for userid in userids:
return binascii.a2b_hex(userkey[32:]) userkey = hashlib.sha256(deviceid + userid).hexdigest()
userkeys.append(binascii.a2b_hex(userkey[32:]))
# This version is used for 3.17.0 and later.
deviceid = hashlib.sha256('XzUhGYdFp' + macaddr).hexdigest()
for userid in userids:
userkey = hashlib.sha256(deviceid + userid).hexdigest()
userkeys.append(binascii.a2b_hex(userkey[32:]))
return userkeys
class KoboBook(object): class KoboBook(object):
"""A Kobo book. """A Kobo book.
@@ -407,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
@@ -435,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():
@@ -473,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 @@
Welcome to the tools! Welcome to the tools!
===================== =====================
This ReadMe_First.txt is meant to give users a quick overview of what is available and how to get started. This document is part of the Tools v6.3.1 archive from Apprentice Alf's Blog: http://apprenticealf.wordpress.com/ This ReadMe_First.txt is meant to give users a quick overview of what is available and how to get started. This document is part of the Tools v6.3.4 archive from Apprentice Alf's Blog: http://apprenticealf.wordpress.com/
The is archive includes tools to remove DRM from: The is archive includes tools to remove DRM from: