Change to unicode strings to fix stand-alone character encoding problems

This commit is contained in:
apprenticeharper 2015-09-30 07:39:29 +01:00
parent b1cccf4b25
commit 4ea0d81144
4 changed files with 112 additions and 66 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, 5) 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'

View File

@ -1,6 +1,10 @@
#!/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 # Version 3.1.5 September 2015
# Removed requirement that a purchase has been made. # Removed requirement that a purchase has been made.
# Also add in character encoding fixes # Also add in character encoding fixes
@ -119,7 +123,7 @@
# #
"""Manage all Kobo books, either encrypted or DRM-free.""" """Manage all Kobo books, either encrypted or DRM-free."""
__version__ = '3.1.5' __version__ = '3.1.6'
import sys import sys
import os import os
@ -246,36 +250,41 @@ class KoboLibrary(object):
def __init__ (self, serials = [], device_path = None): 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__)
self.kobodir = '' self.kobodir = u""
kobodb = '' kobodb = u""
# - first check whether serials have been found or are provided # - first check whether serials have been found or are provided
# and a device is connected. In this case, use the device # and a device is connected. In this case, use the device
# - otherwise fall back to Kobo Desktop Application for Windows and Mac # - otherwise fall back to Kobo Desktop Application for Windows and Mac
if (device_path and (len(serials) > 0)): if (device_path and (len(serials) > 0)):
self.kobodir = os.path.join(device_path, '.kobo') self.kobodir = os.path.join(device_path, u".kobo")
# devices use KoboReader.sqlite # devices use KoboReader.sqlite
kobodb = os.path.join(self.kobodir, 'KoboReader.sqlite') kobodb = os.path.join(self.kobodir, u"KoboReader.sqlite")
if (not(os.path.exists(kobodb))): if (not(os.path.exists(kobodb))):
# give up here, we haven't found anything useful # give up here, we haven't found anything useful
self.kobodir = '' self.kobodir = u""
kobodb = '' kobodb = u""
if (self.kobodir == ''): if (self.kobodir == u""):
# we haven't found a device with serials, so try desktop apps # we haven't found a device with serials, so try desktop apps
if sys.platform.startswith('win'): if sys.platform.startswith('win'):
import _winreg as winreg
if sys.getwindowsversion().major > 5: if sys.getwindowsversion().major > 5:
self.kobodir = os.environ['LOCALAPPDATA'] if 'LOCALAPPDATA' in os.environ.keys():
else: # Python 2.x does not return unicode env. Use Python 3.x
self.kobodir = os.path.join(os.environ['USERPROFILE'], 'Local Settings', 'Application Data') self.kobodir = winreg.ExpandEnvironmentStrings(u"%LOCALAPPDATA%")
self.kobodir = os.path.join(self.kobodir, 'Kobo', 'Kobo Desktop Edition') 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'): elif sys.platform.startswith('darwin'):
self.kobodir = os.path.join(os.environ['HOME'], 'Library', 'Application Support', 'Kobo', 'Kobo Desktop Edition') self.kobodir = os.path.join(os.environ['HOME'], u"Library", u"Application Support", u"Kobo", u"Kobo Desktop Edition")
# desktop versions use Kobo.sqlite # desktop versions use Kobo.sqlite
kobodb = os.path.join(self.kobodir, 'Kobo.sqlite') kobodb = os.path.join(self.kobodir, u"Kobo.sqlite")
if (self.kobodir != ''): if (self.kobodir != u""):
self.bookdir = os.path.join(self.kobodir, 'kepub') self.bookdir = os.path.join(self.kobodir, u"kepub")
self.__sqlite = sqlite3.connect(kobodb) self.__sqlite = sqlite3.connect(kobodb)
self.__cursor = self.__sqlite.cursor() self.__cursor = self.__sqlite.cursor()
self._userkeys = [] self._userkeys = []
@ -322,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."""
@ -338,7 +347,7 @@ 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 # extend the list of macaddrs in any case with the serials
@ -480,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
@ -512,31 +521,30 @@ 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():
@ -545,20 +553,22 @@ def cli_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()
if result != 0:
print u"Could not decrypt book with any of the keys found."
return result return result
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -1,6 +1,10 @@
#!/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 # Version 3.1.5 September 2015
# Removed requirement that a purchase has been made. # Removed requirement that a purchase has been made.
# Also add in character encoding fixes # Also add in character encoding fixes
@ -119,7 +123,7 @@
# #
"""Manage all Kobo books, either encrypted or DRM-free.""" """Manage all Kobo books, either encrypted or DRM-free."""
__version__ = '3.1.5' __version__ = '3.1.6'
import sys import sys
import os import os
@ -244,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."""
@ -301,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."""
@ -317,8 +347,13 @@ 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):
@ -454,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
@ -486,31 +521,30 @@ 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)) 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():
@ -519,20 +553,22 @@ def cli_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()
if result != 0:
print u"Could not decrypt book with any of the keys found."
return result return result
if __name__ == '__main__': if __name__ == '__main__':