More generic 3.0 changes, to be tested.

This commit is contained in:
Apprentice Harper
2020-09-27 11:54:49 +01:00
parent 6920f79a26
commit de50a02af9
42 changed files with 882 additions and 1028 deletions

View File

@@ -1,8 +1,8 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
_license__ = 'GPL v3'
__docformat__ = 'restructuredtext en'

View File

@@ -1,7 +1,6 @@
#!/usr/bin/env python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2012, David Forrester <davidfor@internode.on.net>'
@@ -9,13 +8,7 @@ __docformat__ = 'restructuredtext en'
import os, time, re, sys
from datetime import datetime
try:
from PyQt5.Qt import (Qt, QIcon, QPixmap, QLabel, QDialog, QHBoxLayout, QProgressBar,
QTableWidgetItem, QFont, QLineEdit, QComboBox,
QVBoxLayout, QDialogButtonBox, QStyledItemDelegate, QDateTime,
QRegExpValidator, QRegExp, QDate, QDateEdit)
except ImportError:
from PyQt4.Qt import (Qt, QIcon, QPixmap, QLabel, QDialog, QHBoxLayout, QProgressBar,
from PyQt5.Qt import (Qt, QIcon, QPixmap, QLabel, QDialog, QHBoxLayout, QProgressBar,
QTableWidgetItem, QFont, QLineEdit, QComboBox,
QVBoxLayout, QDialogButtonBox, QStyledItemDelegate, QDateTime,
QRegExpValidator, QRegExp, QDate, QDateEdit)

View File

@@ -1,16 +1,10 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
try:
from PyQt5.Qt import (Qt, QGroupBox, QListWidget, QLineEdit, QDialogButtonBox, QWidget, QLabel, QDialog, QVBoxLayout, QAbstractItemView, QIcon, QHBoxLayout, QComboBox, QListWidgetItem, QFileDialog)
except ImportError:
from PyQt4.Qt import (Qt, QGroupBox, QListWidget, QLineEdit, QDialogButtonBox, QWidget, QLabel, QDialog, QVBoxLayout, QAbstractItemView, QIcon, QHBoxLayout, QComboBox, QListWidgetItem, QFileDialog)
try:
from PyQt5 import Qt as QtGui
except ImportError:
from PyQt4 import QtGui
from PyQt5.Qt import (Qt, QGroupBox, QListWidget, QLineEdit, QDialogButtonBox, QWidget, QLabel, QDialog, QVBoxLayout, QAbstractItemView, QIcon, QHBoxLayout, QComboBox, QListWidgetItem, QFileDialog)
from PyQt5 import Qt as QtGui
from calibre.gui2 import (error_dialog, question_dialog, info_dialog, open_url)
from calibre.utils.config import JSONConfig, config_dir
@@ -50,25 +44,25 @@ class ConfigWidget(QWidget):
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.setToolTip(_("Click to manage Kobo serial numbers for Kobo ebooks"))
self.serials_button.setText("Kobo devices serials")
self.serials_button.clicked.connect(self.edit_serials)
layout.addWidget(self.serials_button)
self.kobo_directory_button = QtGui.QPushButton(self)
self.kobo_directory_button.setToolTip(_(u"Click to specify the Kobo directory"))
self.kobo_directory_button.setText(u"Kobo directory")
self.kobo_directory_button.setToolTip(_("Click to specify the Kobo directory"))
self.kobo_directory_button.setText("Kobo directory")
self.kobo_directory_button.clicked.connect(self.edit_kobo_directory)
layout.addWidget(self.kobo_directory_button)
def edit_serials(self):
d = ManageKeysDialog(self,u"Kobo device serial number",self.tmpserials, AddSerialDialog)
d = ManageKeysDialog(self,"Kobo device serial number",self.tmpserials, AddSerialDialog)
d.exec_()
def edit_kobo_directory(self):
tmpkobodirectory = QFileDialog.getExistingDirectory(self, u"Select Kobo directory", self.kobodirectory or "/home", QFileDialog.ShowDirsOnly)
tmpkobodirectory = QFileDialog.getExistingDirectory(self, "Select Kobo directory", self.kobodirectory or "/home", QFileDialog.ShowDirsOnly)
if tmpkobodirectory != u"" and tmpkobodirectory is not None:
self.kobodirectory = tmpkobodirectory
@@ -91,7 +85,7 @@ class ManageKeysDialog(QDialog):
self.plugin_keys = plugin_keys
self.create_key = create_key
self.keyfile_ext = keyfile_ext
self.json_file = (keyfile_ext == u"k4i")
self.json_file = (keyfile_ext == "k4i")
self.setWindowTitle("{0} {1}: Manage {2}s".format(PLUGIN_NAME, PLUGIN_VERSION, self.key_type_name))
@@ -99,13 +93,13 @@ class ManageKeysDialog(QDialog):
layout = QVBoxLayout(self)
self.setLayout(layout)
keys_group_box = QGroupBox(_(u"{0}s".format(self.key_type_name)), self)
keys_group_box = QGroupBox(_("{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.setToolTip("{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)
@@ -114,12 +108,12 @@ class ManageKeysDialog(QDialog):
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.setToolTip("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.setToolTip(_("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)
@@ -155,7 +149,7 @@ class ManageKeysDialog(QDialog):
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)
"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)
@@ -166,7 +160,7 @@ class ManageKeysDialog(QDialog):
if not self.listy.currentItem():
return
keyname = 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):
if not question_dialog(self, "{0} {1}: Confirm Delete".format(PLUGIN_NAME, PLUGIN_VERSION), "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)
@@ -177,7 +171,7 @@ 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))
self.setWindowTitle("{0} {1}: Add New eInk Kobo Serial Number".format(PLUGIN_NAME, PLUGIN_VERSION))
layout = QVBoxLayout(self)
self.setLayout(layout)
@@ -188,9 +182,9 @@ class AddSerialDialog(QDialog):
key_group = QHBoxLayout()
data_group_box_layout.addLayout(key_group)
key_group.addWidget(QLabel(u"EInk Kobo Serial Number:", self))
key_group.addWidget(QLabel("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.")
self.key_ledit.setToolTip("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)
@@ -210,9 +204,9 @@ class AddSerialDialog(QDialog):
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."
errmsg = "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))
errmsg = "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,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Version 4.0.0 September 2020
@@ -156,7 +156,7 @@
from __future__ import print_function
__version__ = '4.0.0'
__about__ = u"Obok v{0}\nCopyright © 2012-2020 Physisticated et al.".format(__version__)
__about__ = "Obok v{0}\nCopyright © 2012-2020 Physisticated et al.".format(__version__)
import sys
import os
@@ -176,10 +176,10 @@ import tempfile
can_parse_xml = True
try:
from xml.etree import ElementTree as ET
# print u"using xml.etree for xml parsing"
# print "using xml.etree for xml parsing"
except ImportError:
can_parse_xml = False
# print u"Cannot find xml.etree, disabling extraction of serial numbers"
# print "Cannot find xml.etree, disabling extraction of serial numbers"
# List of all known hash keys
KOBO_HASH_KEYS = ['88b3a2e13', 'XzUhGYdFp', 'NoCanLook','QJhwzAtXL']
@@ -279,10 +279,10 @@ class SafeUnbuffered:
if self.encoding == None:
self.encoding = "utf-8"
def write(self, data):
if isinstance(data,bytes):
if isinstance(data,str):
data = data.encode(self.encoding,"replace")
self.stream.write(data)
self.stream.flush()
self.stream.buffer.write(data)
self.stream.buffer.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
@@ -312,9 +312,9 @@ class KoboLibrary(object):
# step 1. check whether this looks like a real device
if (device_path):
# we got a device path
self.kobodir = os.path.join(device_path, u".kobo")
self.kobodir = os.path.join(device_path, ".kobo")
# devices use KoboReader.sqlite
kobodb = os.path.join(self.kobodir, u"KoboReader.sqlite")
kobodb = os.path.join(self.kobodir, "KoboReader.sqlite")
if (not(os.path.isfile(kobodb))):
# device path seems to be wrong, unset it
device_path = u""
@@ -326,22 +326,22 @@ class KoboLibrary(object):
if (len(serials) == 0):
# we got a device path but no saved serial
# try to get the serial from the device
# print u"get_device_settings - device_path = {0}".format(device_path)
# print "get_device_settings - device_path = {0}".format(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')
# print u"trying to load {0}".format(devicexml)
# print "trying to load {0}".format(devicexml)
if (os.path.exists(devicexml)):
# print u"trying to parse {0}".format(devicexml)
# print "trying to parse {0}".format(devicexml)
xmltree = ET.parse(devicexml)
for node in xmltree.iter():
if "deviceSerial" in node.tag:
serial = node.text
# print u"found serial {0}".format(serial)
# print "found serial {0}".format(serial)
serials.append(serial)
break
else:
# print u"cannot get serials from device."
# print "cannot get serials from device."
device_path = u""
self.kobodir = u""
kobodb = u""
@@ -357,19 +357,19 @@ class KoboLibrary(object):
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%")
self.kobodir = winreg.ExpandEnvironmentStrings("%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")
self.kobodir = os.path.join(winreg.ExpandEnvironmentStrings("%USERPROFILE%"), "Local Settings", "Application Data")
self.kobodir = os.path.join(self.kobodir, "Kobo", "Kobo Desktop Edition")
elif sys.platform.startswith('darwin'):
self.kobodir = os.path.join(os.environ['HOME'], u"Library", u"Application Support", u"Kobo", u"Kobo Desktop Edition")
self.kobodir = os.path.join(os.environ['HOME'], "Library", "Application Support", "Kobo", "Kobo Desktop Edition")
#elif linux_path != None:
# Probably Linux, let's get the wine prefix and path to Kobo.
# self.kobodir = os.path.join(linux_path, u"Local Settings", u"Application Data", u"Kobo", u"Kobo Desktop Edition")
# self.kobodir = os.path.join(linux_path, "Local Settings", "Application Data", "Kobo", "Kobo Desktop Edition")
# desktop versions use Kobo.sqlite
kobodb = os.path.join(self.kobodir, u"Kobo.sqlite")
kobodb = os.path.join(self.kobodir, "Kobo.sqlite")
# check for existence of file
if (not(os.path.isfile(kobodb))):
# give up here, we haven't found anything useful
@@ -377,7 +377,7 @@ class KoboLibrary(object):
kobodb = u""
if (self.kobodir != u""):
self.bookdir = os.path.join(self.kobodir, u"kepub")
self.bookdir = os.path.join(self.kobodir, "kepub")
# make a copy of the database in a temporary file
# so we can ensure it's not using WAL logging which sqlite3 can't do.
self.newdb = tempfile.NamedTemporaryFile(mode='wb', delete=False)
@@ -437,7 +437,7 @@ class KoboLibrary(object):
def __bookfile (self, volumeid):
"""The filename needed to open a given book."""
return os.path.join(self.kobodir, u"kepub", volumeid)
return os.path.join(self.kobodir, "kepub", volumeid)
def __getmacaddrs (self):
"""The list of all MAC addresses on this machine."""
@@ -454,7 +454,7 @@ class KoboLibrary(object):
output = subprocess.check_output('/sbin/ifconfig -a', shell=True)
matches = c.findall(output)
for m in matches:
# print u"m:{0}".format(m[0])
# print "m:{0}".format(m[0])
macaddrs.append(m[0].upper())
else:
# probably linux
@@ -607,32 +607,32 @@ class KoboFile(object):
# assume utf-8 with no BOM
textoffset = 0
stride = 1
print(u"Checking text:{0}:".format(contents[:10]))
print("Checking text:{0}:".format(contents[:10]))
# check for byte order mark
if contents[:3]==b"\xef\xbb\xbf":
# seems to be utf-8 with BOM
print(u"Could be utf-8 with BOM")
print("Could be utf-8 with BOM")
textoffset = 3
elif contents[:2]==b"\xfe\xff":
# seems to be utf-16BE
print(u"Could be utf-16BE")
print("Could be utf-16BE")
textoffset = 3
stride = 2
elif contents[:2]==b"\xff\xfe":
# seems to be utf-16LE
print(u"Could be utf-16LE")
print("Could be utf-16LE")
textoffset = 2
stride = 2
else:
print(u"Perhaps utf-8 without BOM")
print("Perhaps utf-8 without BOM")
# now check that the first few characters are in the ASCII range
for i in range(textoffset,textoffset+5*stride,stride):
if contents[i]<32 or contents[i]>127:
# Non-ascii, so decryption probably failed
print(u"Bad character at {0}, value {1}".format(i,contents[i]))
print("Bad character at {0}, value {1}".format(i,contents[i]))
raise ValueError
print(u"Seems to be good text")
print("Seems to be good text")
return True
if contents[:5]==b"<?xml" or contents[:8]==b"\xef\xbb\xbf<?xml":
# utf-8
@@ -653,13 +653,13 @@ class KoboFile(object):
# utf-16LE of weird <!DOCTYPE start
return True
else:
print(u"Bad XML: {0}".format(contents[:8]))
print("Bad XML: {0}".format(contents[:8]))
raise ValueError
elif self.mimetype == 'image/jpeg':
if contents[:3] == b'\xff\xd8\xff':
return True
else:
print(u"Bad JPEG: {0}".format(contents[:3].hex()))
print("Bad JPEG: {0}".format(contents[:3].hex()))
raise ValueError()
return False
@@ -682,18 +682,18 @@ class KoboFile(object):
return contents
def decrypt_book(book, lib):
print(u"Converting {0}".format(book.title))
print("Converting {0}".format(book.title))
zin = zipfile.ZipFile(book.filename, "r")
# make filename out of Unicode alphanumeric and whitespace equivalents from title
outname = u"{0}.epub".format(re.sub('[^\s\w]', '_', book.title, 0, re.UNICODE))
outname = "{0}.epub".format(re.sub('[^\s\w]', '_', book.title, 0, re.UNICODE))
if (book.type == 'drm-free'):
print(u"DRM-free book, conversion is not needed")
print("DRM-free book, conversion is not needed")
shutil.copyfile(book.filename, outname)
print(u"Book saved as {0}".format(os.path.join(os.getcwd(), outname)))
print("Book saved as {0}".format(os.path.join(os.getcwd(), outname)))
return 0
result = 1
for userkey in lib.userkeys:
print(u"Trying key: {0}".format(userkey.hex()))
print("Trying key: {0}".format(userkey.hex()))
try:
zout = zipfile.ZipFile(outname, "w", zipfile.ZIP_DEFLATED)
for filename in zin.namelist():
@@ -705,12 +705,12 @@ def decrypt_book(book, lib):
file.check(contents)
zout.writestr(filename, contents)
zout.close()
print(u"Decryption succeeded.")
print(u"Book saved as {0}".format(os.path.join(os.getcwd(), outname)))
print("Decryption succeeded.")
print("Book saved as {0}".format(os.path.join(os.getcwd(), outname)))
result = 0
break
except ValueError:
print(u"Decryption failed.")
print("Decryption failed.")
zout.close()
os.remove(outname)
zin.close()
@@ -719,7 +719,7 @@ def decrypt_book(book, lib):
def cli_main():
description = __about__
epilog = u"Parsing of arguments failed."
epilog = "Parsing of arguments failed."
parser = argparse.ArgumentParser(prog=sys.argv[0], description=description, epilog=epilog)
parser.add_argument('--devicedir', default='/media/KOBOeReader', help="directory of connected Kobo device")
parser.add_argument('--all', action='store_true', help="flag for converting all books on device")
@@ -735,25 +735,25 @@ def cli_main():
books = lib.books
else:
for i, book in enumerate(lib.books):
print(u"{0}: {1}".format(i + 1, book.title))
print(u"Or 'all'")
print("{0}: {1}".format(i + 1, book.title))
print("Or 'all'")
choice = input(u"Convert book number... ")
if choice == u'all':
choice = input("Convert book number... ")
if choice == "all":
books = list(lib.books)
else:
try:
num = int(choice)
books = [lib.books[num - 1]]
except (ValueError, IndexError):
print(u"Invalid choice. Exiting...")
print("Invalid choice. Exiting...")
exit()
results = [decrypt_book(book, lib) for book in books]
lib.close()
overall_result = all(result != 0 for result in results)
if overall_result != 0:
print(u"Could not decrypt book with any of the keys found.")
print("Could not decrypt book with any of the keys found.")
return overall_result

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__docformat__ = 'restructuredtext en'
@@ -13,10 +13,7 @@ except ImportError:
from io import StringIO
from traceback import print_exc
try:
from PyQt5.Qt import (Qt, QDialog, QPixmap, QIcon, QLabel, QHBoxLayout, QFont, QTableWidgetItem)
except ImportError:
from PyQt4.Qt import (Qt, QDialog, QPixmap, QIcon, QLabel, QHBoxLayout, QFont, QTableWidgetItem)
from PyQt5.Qt import (Qt, QDialog, QPixmap, QIcon, QLabel, QHBoxLayout, QFont, QTableWidgetItem)
from calibre.utils.config import config_dir
from calibre.constants import iswindows, DEBUG