From a30aace99c61c30050f8fee8c3444f2d355c8bd6 Mon Sep 17 00:00:00 2001 From: Apprentice Alf Date: Sat, 7 Mar 2015 21:18:50 +0000 Subject: [PATCH] tools v6.0.9 obok added to other tools --- .../DeDRM.app/Contents/Info.plist | 6 +- .../Contents/Resources/DeDRM_Help.htm | 2 +- .../DeDRM.app/Contents/Resources/__init__.py | 3 +- .../DeDRM.app/Contents/Resources/config.py | 76 +++--- .../DeDRM_App/DeDRM_lib/lib/DeDRM_Help.htm | 2 +- .../DeDRM_App/DeDRM_lib/lib/__init__.py | 3 +- .../DeDRM_App/DeDRM_lib/lib/config.py | 76 +++--- DeDRM_calibre_plugin/DeDRM_plugin.zip | Bin 336887 -> 336861 bytes .../DeDRM_plugin/DeDRM_Help.htm | 2 +- DeDRM_calibre_plugin/DeDRM_plugin/__init__.py | 3 +- DeDRM_calibre_plugin/DeDRM_plugin/config.py | 76 +++--- .../kindle4.0.2.1.patch | 238 ++++++++++++++++++ Other_Tools/Kobo/obok_2.01.py | 231 +++++++++++++++++ 13 files changed, 604 insertions(+), 114 deletions(-) create mode 100644 Other_Tools/Kindle_for_Android_Patches/kindle_version_4.0.2.1/kindle4.0.2.1.patch create mode 100644 Other_Tools/Kobo/obok_2.01.py diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Info.plist b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Info.plist index cfd9fcc7..6ed5ade0 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Info.plist +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Info.plist @@ -24,19 +24,19 @@ CFBundleExecutable droplet CFBundleGetInfoString - DeDRM AppleScript 6.0.8. Written 2010–2013 by Apprentice Alf and others. + DeDRM AppleScript 6.0.9. Written 2010–2013 by Apprentice Alf and others. CFBundleIconFile DeDRM CFBundleIdentifier com.apple.ScriptEditor.id.707CCCD5-0C6C-4BEB-B67C-B6E866ADE85A CFBundleInfoDictionaryVersion - 6.0.8 + 6.0.9 CFBundleName DeDRM CFBundlePackageType APPL CFBundleShortVersionString - 6.0.8 + 6.0.9 CFBundleSignature dplt LSRequiresCarbon diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_Help.htm b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_Help.htm index 69edade3..f0c51a8d 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_Help.htm +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM_Help.htm @@ -17,7 +17,7 @@ -

DeDRM Plugin (v6.0.0)

+

DeDRM Plugin (v6.0.9)

This plugin removes DRM from ebooks when they are imported into calibre. If you already have DRMed ebooks in your calibre library, you will need to remove them and import them again.

diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/__init__.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/__init__.py index 37d4cb15..232b6bc9 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/__init__.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/__init__.py @@ -35,13 +35,14 @@ # 6.0.6 - Fix up an incorrect function call # 6.0.7 - Error handling for incomplete PDF metadata # 6.0.8 - Fixes a Wine key issue and topaz support +# 6.0.9 - Ported to work with newer versions of Calibre (moved to Qt5). Still supports older Qt4 versions. """ Decrypt DRMed ebooks. """ PLUGIN_NAME = u"DeDRM" -PLUGIN_VERSION_TUPLE = (6, 0, 8) +PLUGIN_VERSION_TUPLE = (6, 0, 9) 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. RESOURCE_NAME = PLUGIN_NAME + '_Help.htm' diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/config.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/config.py index 1e584768..b5c5300d 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/config.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/config.py @@ -9,16 +9,24 @@ import os, traceback, json # PyQT4 modules (part of calibre). -from PyQt4.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit, +try: + from PyQt5.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit, QGroupBox, QPushButton, QListWidget, QListWidgetItem, - QAbstractItemView, QIcon, QDialog, QDialogButtonBox, QUrl, QString) -from PyQt4 import QtGui - + QAbstractItemView, QIcon, QDialog, QDialogButtonBox, QUrl) +except ImportError: + from PyQt4.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit, + QGroupBox, QPushButton, QListWidget, QListWidgetItem, + QAbstractItemView, QIcon, QDialog, QDialogButtonBox, QUrl) +try: + from PyQt5 import Qt as QtGui +except ImportError: + from PyQt4 import QtGui + from zipfile import ZipFile # calibre modules and constants. from calibre.gui2 import (error_dialog, question_dialog, info_dialog, open_url, - choose_dir, choose_files) + choose_dir, choose_files, choose_save_file) from calibre.utils.config import dynamic, config_dir, JSONConfig from calibre.constants import iswindows, isosx @@ -267,7 +275,7 @@ def __init__(self, parent, key_type_name, plugin_keys, create_key, keyfile_ext = def getwineprefix(self): if self.wineprefix is not None: - return unicode(self.wp_lineedit.text().toUtf8(), 'utf8').strip() + return unicode(self.wp_lineedit.text()).strip() return u"" def populate_list(self): @@ -316,7 +324,7 @@ def rename_key(self): if d.result() != d.Accepted: # rename cancelled or moot. return - keyname = unicode(self.listy.currentItem().text().toUtf8(),'utf8') + 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 {0} to {1}?".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] @@ -328,7 +336,7 @@ def rename_key(self): def delete_key(self): if not self.listy.currentItem(): return - keyname = unicode(self.listy.currentItem().text().toUtf8(), 'utf8') + 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} {0}?".format(keyname, self.key_type_name), show_copy_button=False, default_yes=False): return if type(self.plugin_keys) == dict: @@ -352,9 +360,10 @@ def get_help_file_resource(): open_url(QUrl(url)) def migrate_files(self): - dynamic[PLUGIN_NAME + u"config_dir"] = config_dir - files = choose_files(self, PLUGIN_NAME + u"config_dir", - u"Select {0} files to import".format(self.key_type_name), [(u"{0} files".format(self.key_type_name), [self.keyfile_ext])], False) + unique_dlg_name = PLUGIN_NAME + u"import {0} keys".format(self.key_type_name).replace(' ', '_') #takes care of automatically remembering last directory + caption = u"Select {0} files to import".format(self.key_type_name) + filters = [(u"{0} files".format(self.key_type_name), [self.keyfile_ext])] + files = choose_files(self, unique_dlg_name, caption, filters, all_files=False) counter = 0 skipped = 0 if files: @@ -408,17 +417,14 @@ def export_key(self): r = error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), _(errmsg), show=True, show_copy_button=False) return - filter = QString(u"{0} Files (*.{1})".format(self.key_type_name, self.keyfile_ext)) - keyname = unicode(self.listy.currentItem().text().toUtf8(), 'utf8') - if dynamic.get(PLUGIN_NAME + 'save_dir'): - defaultname = os.path.join(dynamic.get(PLUGIN_NAME + 'save_dir'), u"{0}.{1}".format(keyname , self.keyfile_ext)) - else: - defaultname = os.path.join(os.path.expanduser('~'), u"{0}.{1}".format(keyname , self.keyfile_ext)) - filename = unicode(QtGui.QFileDialog.getSaveFileName(self, u"Save {0} File as...".format(self.key_type_name), defaultname, - u"{0} Files (*.{1})".format(self.key_type_name,self.keyfile_ext), filter)) + keyname = unicode(self.listy.currentItem().text()) + unique_dlg_name = PLUGIN_NAME + u"export {0} keys".format(self.key_type_name).replace(' ', '_') #takes care of automatically remembering last directory + caption = u"Save {0} File as...".format(self.key_type_name) + filters = [(u"{0} Files".format(self.key_type_name), [u"{0}".format(self.keyfile_ext)])] + defaultname = u"{0}.{1}".format(keyname, self.keyfile_ext) + filename = choose_save_file(self, unique_dlg_name, caption, filters, all_files=False, initial_filename=defaultname) if filename: - dynamic[PLUGIN_NAME + 'save_dir'] = os.path.split(filename)[0] - with file(filename, 'w') as fname: + with file(filename, 'wb') as fname: if self.binary_file: fname.write(self.plugin_keys[keyname].decode('hex')) elif self.json_file: @@ -458,7 +464,7 @@ def __init__(self, parent=None,): self.resize(self.sizeHint()) def accept(self): - if self.key_ledit.text().isEmpty() or unicode(self.key_ledit.text()).isspace(): + if not unicode(self.key_ledit.text()) or unicode(self.key_ledit.text()).isspace(): errmsg = u"Key name field cannot be empty!" return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), _(errmsg), show=True, show_copy_button=False) @@ -479,7 +485,7 @@ def accept(self): @property def key_name(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() @@ -553,7 +559,7 @@ def __init__(self, parent=None,): @property def key_name(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() @property def key_value(self): @@ -562,11 +568,11 @@ def key_value(self): @property def user_name(self): - return unicode(self.name_ledit.text().toUtf8(), 'utf8').strip().lower().replace(' ','') + return unicode(self.name_ledit.text()).strip().lower().replace(' ','') @property def cc_number(self): - return unicode(self.cc_ledit.text().toUtf8(), 'utf8').strip().replace(' ', '').replace('-','') + return unicode(self.cc_ledit.text()).strip().replace(' ', '').replace('-','') def accept(self): @@ -634,7 +640,7 @@ def __init__(self, parent=None,): @property def key_name(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() @property def key_value(self): @@ -643,11 +649,11 @@ def key_value(self): @property def user_name(self): - return unicode(self.name_ledit.text().toUtf8(), 'utf8').strip().lower().replace(' ','') + return unicode(self.name_ledit.text()).strip().lower().replace(' ','') @property def cc_number(self): - return unicode(self.cc_ledit.text().toUtf8(), 'utf8').strip().replace(' ', '').replace('-','') + return unicode(self.cc_ledit.text()).strip().replace(' ', '').replace('-','') def accept(self): @@ -719,7 +725,7 @@ def __init__(self, parent=None,): @property def key_name(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() @property def key_value(self): @@ -792,7 +798,7 @@ def __init__(self, parent=None,): @property def key_name(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() @property def key_value(self): @@ -841,11 +847,11 @@ def __init__(self, parent=None,): @property def key_name(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() @property def key_value(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() def accept(self): if len(self.key_name) == 0 or self.key_name.isspace(): @@ -889,11 +895,11 @@ def __init__(self, parent=None,): @property def key_name(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() @property def key_value(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() def accept(self): if len(self.key_name) == 0 or self.key_name.isspace(): diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/DeDRM_Help.htm b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/DeDRM_Help.htm index 69edade3..f0c51a8d 100644 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/DeDRM_Help.htm +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/DeDRM_Help.htm @@ -17,7 +17,7 @@ -

DeDRM Plugin (v6.0.0)

+

DeDRM Plugin (v6.0.9)

This plugin removes DRM from ebooks when they are imported into calibre. If you already have DRMed ebooks in your calibre library, you will need to remove them and import them again.

diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/__init__.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/__init__.py index 37d4cb15..232b6bc9 100644 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/__init__.py +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/__init__.py @@ -35,13 +35,14 @@ # 6.0.6 - Fix up an incorrect function call # 6.0.7 - Error handling for incomplete PDF metadata # 6.0.8 - Fixes a Wine key issue and topaz support +# 6.0.9 - Ported to work with newer versions of Calibre (moved to Qt5). Still supports older Qt4 versions. """ Decrypt DRMed ebooks. """ PLUGIN_NAME = u"DeDRM" -PLUGIN_VERSION_TUPLE = (6, 0, 8) +PLUGIN_VERSION_TUPLE = (6, 0, 9) 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. RESOURCE_NAME = PLUGIN_NAME + '_Help.htm' diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/config.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/config.py index 1e584768..b5c5300d 100644 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/config.py +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/config.py @@ -9,16 +9,24 @@ import os, traceback, json # PyQT4 modules (part of calibre). -from PyQt4.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit, +try: + from PyQt5.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit, QGroupBox, QPushButton, QListWidget, QListWidgetItem, - QAbstractItemView, QIcon, QDialog, QDialogButtonBox, QUrl, QString) -from PyQt4 import QtGui - + QAbstractItemView, QIcon, QDialog, QDialogButtonBox, QUrl) +except ImportError: + from PyQt4.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit, + QGroupBox, QPushButton, QListWidget, QListWidgetItem, + QAbstractItemView, QIcon, QDialog, QDialogButtonBox, QUrl) +try: + from PyQt5 import Qt as QtGui +except ImportError: + from PyQt4 import QtGui + from zipfile import ZipFile # calibre modules and constants. from calibre.gui2 import (error_dialog, question_dialog, info_dialog, open_url, - choose_dir, choose_files) + choose_dir, choose_files, choose_save_file) from calibre.utils.config import dynamic, config_dir, JSONConfig from calibre.constants import iswindows, isosx @@ -267,7 +275,7 @@ def __init__(self, parent, key_type_name, plugin_keys, create_key, keyfile_ext = def getwineprefix(self): if self.wineprefix is not None: - return unicode(self.wp_lineedit.text().toUtf8(), 'utf8').strip() + return unicode(self.wp_lineedit.text()).strip() return u"" def populate_list(self): @@ -316,7 +324,7 @@ def rename_key(self): if d.result() != d.Accepted: # rename cancelled or moot. return - keyname = unicode(self.listy.currentItem().text().toUtf8(),'utf8') + 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 {0} to {1}?".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] @@ -328,7 +336,7 @@ def rename_key(self): def delete_key(self): if not self.listy.currentItem(): return - keyname = unicode(self.listy.currentItem().text().toUtf8(), 'utf8') + 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} {0}?".format(keyname, self.key_type_name), show_copy_button=False, default_yes=False): return if type(self.plugin_keys) == dict: @@ -352,9 +360,10 @@ def get_help_file_resource(): open_url(QUrl(url)) def migrate_files(self): - dynamic[PLUGIN_NAME + u"config_dir"] = config_dir - files = choose_files(self, PLUGIN_NAME + u"config_dir", - u"Select {0} files to import".format(self.key_type_name), [(u"{0} files".format(self.key_type_name), [self.keyfile_ext])], False) + unique_dlg_name = PLUGIN_NAME + u"import {0} keys".format(self.key_type_name).replace(' ', '_') #takes care of automatically remembering last directory + caption = u"Select {0} files to import".format(self.key_type_name) + filters = [(u"{0} files".format(self.key_type_name), [self.keyfile_ext])] + files = choose_files(self, unique_dlg_name, caption, filters, all_files=False) counter = 0 skipped = 0 if files: @@ -408,17 +417,14 @@ def export_key(self): r = error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), _(errmsg), show=True, show_copy_button=False) return - filter = QString(u"{0} Files (*.{1})".format(self.key_type_name, self.keyfile_ext)) - keyname = unicode(self.listy.currentItem().text().toUtf8(), 'utf8') - if dynamic.get(PLUGIN_NAME + 'save_dir'): - defaultname = os.path.join(dynamic.get(PLUGIN_NAME + 'save_dir'), u"{0}.{1}".format(keyname , self.keyfile_ext)) - else: - defaultname = os.path.join(os.path.expanduser('~'), u"{0}.{1}".format(keyname , self.keyfile_ext)) - filename = unicode(QtGui.QFileDialog.getSaveFileName(self, u"Save {0} File as...".format(self.key_type_name), defaultname, - u"{0} Files (*.{1})".format(self.key_type_name,self.keyfile_ext), filter)) + keyname = unicode(self.listy.currentItem().text()) + unique_dlg_name = PLUGIN_NAME + u"export {0} keys".format(self.key_type_name).replace(' ', '_') #takes care of automatically remembering last directory + caption = u"Save {0} File as...".format(self.key_type_name) + filters = [(u"{0} Files".format(self.key_type_name), [u"{0}".format(self.keyfile_ext)])] + defaultname = u"{0}.{1}".format(keyname, self.keyfile_ext) + filename = choose_save_file(self, unique_dlg_name, caption, filters, all_files=False, initial_filename=defaultname) if filename: - dynamic[PLUGIN_NAME + 'save_dir'] = os.path.split(filename)[0] - with file(filename, 'w') as fname: + with file(filename, 'wb') as fname: if self.binary_file: fname.write(self.plugin_keys[keyname].decode('hex')) elif self.json_file: @@ -458,7 +464,7 @@ def __init__(self, parent=None,): self.resize(self.sizeHint()) def accept(self): - if self.key_ledit.text().isEmpty() or unicode(self.key_ledit.text()).isspace(): + if not unicode(self.key_ledit.text()) or unicode(self.key_ledit.text()).isspace(): errmsg = u"Key name field cannot be empty!" return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), _(errmsg), show=True, show_copy_button=False) @@ -479,7 +485,7 @@ def accept(self): @property def key_name(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() @@ -553,7 +559,7 @@ def __init__(self, parent=None,): @property def key_name(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() @property def key_value(self): @@ -562,11 +568,11 @@ def key_value(self): @property def user_name(self): - return unicode(self.name_ledit.text().toUtf8(), 'utf8').strip().lower().replace(' ','') + return unicode(self.name_ledit.text()).strip().lower().replace(' ','') @property def cc_number(self): - return unicode(self.cc_ledit.text().toUtf8(), 'utf8').strip().replace(' ', '').replace('-','') + return unicode(self.cc_ledit.text()).strip().replace(' ', '').replace('-','') def accept(self): @@ -634,7 +640,7 @@ def __init__(self, parent=None,): @property def key_name(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() @property def key_value(self): @@ -643,11 +649,11 @@ def key_value(self): @property def user_name(self): - return unicode(self.name_ledit.text().toUtf8(), 'utf8').strip().lower().replace(' ','') + return unicode(self.name_ledit.text()).strip().lower().replace(' ','') @property def cc_number(self): - return unicode(self.cc_ledit.text().toUtf8(), 'utf8').strip().replace(' ', '').replace('-','') + return unicode(self.cc_ledit.text()).strip().replace(' ', '').replace('-','') def accept(self): @@ -719,7 +725,7 @@ def __init__(self, parent=None,): @property def key_name(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() @property def key_value(self): @@ -792,7 +798,7 @@ def __init__(self, parent=None,): @property def key_name(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() @property def key_value(self): @@ -841,11 +847,11 @@ def __init__(self, parent=None,): @property def key_name(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() @property def key_value(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() def accept(self): if len(self.key_name) == 0 or self.key_name.isspace(): @@ -889,11 +895,11 @@ def __init__(self, parent=None,): @property def key_name(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() @property def key_value(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() def accept(self): if len(self.key_name) == 0 or self.key_name.isspace(): diff --git a/DeDRM_calibre_plugin/DeDRM_plugin.zip b/DeDRM_calibre_plugin/DeDRM_plugin.zip index 58d817484d8597a28a344d4cfdc375c419fe25b1..b314cb065dcb40946ea70c0767660592b9d9bfa6 100644 GIT binary patch delta 14438 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 delta 14479 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�`Pi&Myx4qG`Jg)`83Z&{uRxHXv>fSF!R^^X#24f3J_A z!CE7c{JjQZ`#~Io0*AHR!x$`O^AV9Z&;2;F_Z1-bLw7upSx)=#YI}Nbq}M_VF$aul~;G%POL#k3U0?z(<@oB|@mZ zf!=)>&`X*yNR#MwFtne01LrGyq(GtqwYM-&KKknPj|R=1A1j4BJ#I9#tPnVW!jokQ z@CC-A{t-o|%&=wZ7x3}I=;z>zar?z_Os<_;d#;_+JUz}B>#@kT$f1@wbf(O)GDlBY z)YS8imSNpNbl#*2M22e>q@8@S#=!miZL>d!;YKSYu1QQ|)c1n52f~Ad z*Vi;x_$+#e;d_>RBT}Z$;u!wOYWQkt+E;F>-Y?T zjGi&wd@3u)&s=sD3T1%sNDtS;YD1e3_ak}|>jh4G=h=zwL2N^DC>o&Hmi;m0A{&rM^0n&kNHXQzX2IVvqW zwfO8KX1@FHCk{DCtC`WzlcLVLyklFW5$HXN!3SQ;54g!F|M=DrO-H|5Ww^A?jAN-W zzNy`MpY;VB*&iu;KjYG)ca57hH6&=L=mAi&6~K0bq{VXKDy$b>F#;fu)}};)&Q{g) zX}bpj9-FoG{7f(D67C%K_#bqc4dR;?xG%cSnMiq%r#JRgI@b?uIv z>Wb{8_MjLRwYK#x(iUK?%+?RTRKw8v$L$q_n}PeELSL4~jT%6#jOx+fnAI};9eR1I z>0qDOO?OkK+Bz=A+FUt~aK|RK-_0>yzFo15wtXM+rxHlLgVXYz z?VoU@GdCC~1wN=lPlaBHbw;1jOZTg5HANP7T^4u57J_+VvQGjB12!6mTWcDa>>VcX z^8Hg+`b*r z#dSA_=dNSI#2ucyPjWECCz~HKGFgd(R=8P-u)*ABJ>*SpM`2 zPI({8mpZ!@+N6~!IcbKcAo*$i;oR*xHLWHk!s3gG%RaWAia=i*bLAeYRv8^eno6Ea zB-<6>W7U(+pS>$O;bW+Y4)9M(b=~bq>p%0ySY9i0Fyp}A+ygJZ@D#U_CEGWR-&jA? zfMOmzr7s0;9Ssm14HPLXY;3@tVsX^{m^@v!7TpNwn}k{P#@T88E(&zLkN2^?H=)6) z*jA>KxO??X+gcMvI#-4fcIQ@2@?P=)+o@?&g2AlaD1+SaD6r=LCNS$reFMi1o_)DN z*|+494dsvwSa-XD_PGBm(A62Z|0xzg9Y|B2f74QVsXSuPywIQ) zP2xRcjzy?e`jzv9ejpOv#@Z7X2E@c)e+q5f8TD2_Z!};jK{Q05Dl7njyJVz)VbRooqc! zOqgAKH6)?HM*l5qX#&7t{(I`JssIi_43=Ufj*ImFt4V@BaR74Qy&zd!fDAYrC=nN+ z0PX07{T60RSHYmIM?>@Xv-Y zXqn)j;T;eqAwUtl5adeuZ&4Z4`Ja>lf*}GZgO`J}iT*)U1QinfgU<*$C;GSd0pcb8 zC((gIiT~{}fTsSNs6e -

DeDRM Plugin (v6.0.0)

+

DeDRM Plugin (v6.0.9)

This plugin removes DRM from ebooks when they are imported into calibre. If you already have DRMed ebooks in your calibre library, you will need to remove them and import them again.

diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/__init__.py b/DeDRM_calibre_plugin/DeDRM_plugin/__init__.py index 37d4cb15..232b6bc9 100644 --- a/DeDRM_calibre_plugin/DeDRM_plugin/__init__.py +++ b/DeDRM_calibre_plugin/DeDRM_plugin/__init__.py @@ -35,13 +35,14 @@ # 6.0.6 - Fix up an incorrect function call # 6.0.7 - Error handling for incomplete PDF metadata # 6.0.8 - Fixes a Wine key issue and topaz support +# 6.0.9 - Ported to work with newer versions of Calibre (moved to Qt5). Still supports older Qt4 versions. """ Decrypt DRMed ebooks. """ PLUGIN_NAME = u"DeDRM" -PLUGIN_VERSION_TUPLE = (6, 0, 8) +PLUGIN_VERSION_TUPLE = (6, 0, 9) 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. RESOURCE_NAME = PLUGIN_NAME + '_Help.htm' diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/config.py b/DeDRM_calibre_plugin/DeDRM_plugin/config.py index 1e584768..b5c5300d 100644 --- a/DeDRM_calibre_plugin/DeDRM_plugin/config.py +++ b/DeDRM_calibre_plugin/DeDRM_plugin/config.py @@ -9,16 +9,24 @@ import os, traceback, json # PyQT4 modules (part of calibre). -from PyQt4.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit, +try: + from PyQt5.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit, QGroupBox, QPushButton, QListWidget, QListWidgetItem, - QAbstractItemView, QIcon, QDialog, QDialogButtonBox, QUrl, QString) -from PyQt4 import QtGui - + QAbstractItemView, QIcon, QDialog, QDialogButtonBox, QUrl) +except ImportError: + from PyQt4.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit, + QGroupBox, QPushButton, QListWidget, QListWidgetItem, + QAbstractItemView, QIcon, QDialog, QDialogButtonBox, QUrl) +try: + from PyQt5 import Qt as QtGui +except ImportError: + from PyQt4 import QtGui + from zipfile import ZipFile # calibre modules and constants. from calibre.gui2 import (error_dialog, question_dialog, info_dialog, open_url, - choose_dir, choose_files) + choose_dir, choose_files, choose_save_file) from calibre.utils.config import dynamic, config_dir, JSONConfig from calibre.constants import iswindows, isosx @@ -267,7 +275,7 @@ def __init__(self, parent, key_type_name, plugin_keys, create_key, keyfile_ext = def getwineprefix(self): if self.wineprefix is not None: - return unicode(self.wp_lineedit.text().toUtf8(), 'utf8').strip() + return unicode(self.wp_lineedit.text()).strip() return u"" def populate_list(self): @@ -316,7 +324,7 @@ def rename_key(self): if d.result() != d.Accepted: # rename cancelled or moot. return - keyname = unicode(self.listy.currentItem().text().toUtf8(),'utf8') + 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 {0} to {1}?".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] @@ -328,7 +336,7 @@ def rename_key(self): def delete_key(self): if not self.listy.currentItem(): return - keyname = unicode(self.listy.currentItem().text().toUtf8(), 'utf8') + 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} {0}?".format(keyname, self.key_type_name), show_copy_button=False, default_yes=False): return if type(self.plugin_keys) == dict: @@ -352,9 +360,10 @@ def get_help_file_resource(): open_url(QUrl(url)) def migrate_files(self): - dynamic[PLUGIN_NAME + u"config_dir"] = config_dir - files = choose_files(self, PLUGIN_NAME + u"config_dir", - u"Select {0} files to import".format(self.key_type_name), [(u"{0} files".format(self.key_type_name), [self.keyfile_ext])], False) + unique_dlg_name = PLUGIN_NAME + u"import {0} keys".format(self.key_type_name).replace(' ', '_') #takes care of automatically remembering last directory + caption = u"Select {0} files to import".format(self.key_type_name) + filters = [(u"{0} files".format(self.key_type_name), [self.keyfile_ext])] + files = choose_files(self, unique_dlg_name, caption, filters, all_files=False) counter = 0 skipped = 0 if files: @@ -408,17 +417,14 @@ def export_key(self): r = error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), _(errmsg), show=True, show_copy_button=False) return - filter = QString(u"{0} Files (*.{1})".format(self.key_type_name, self.keyfile_ext)) - keyname = unicode(self.listy.currentItem().text().toUtf8(), 'utf8') - if dynamic.get(PLUGIN_NAME + 'save_dir'): - defaultname = os.path.join(dynamic.get(PLUGIN_NAME + 'save_dir'), u"{0}.{1}".format(keyname , self.keyfile_ext)) - else: - defaultname = os.path.join(os.path.expanduser('~'), u"{0}.{1}".format(keyname , self.keyfile_ext)) - filename = unicode(QtGui.QFileDialog.getSaveFileName(self, u"Save {0} File as...".format(self.key_type_name), defaultname, - u"{0} Files (*.{1})".format(self.key_type_name,self.keyfile_ext), filter)) + keyname = unicode(self.listy.currentItem().text()) + unique_dlg_name = PLUGIN_NAME + u"export {0} keys".format(self.key_type_name).replace(' ', '_') #takes care of automatically remembering last directory + caption = u"Save {0} File as...".format(self.key_type_name) + filters = [(u"{0} Files".format(self.key_type_name), [u"{0}".format(self.keyfile_ext)])] + defaultname = u"{0}.{1}".format(keyname, self.keyfile_ext) + filename = choose_save_file(self, unique_dlg_name, caption, filters, all_files=False, initial_filename=defaultname) if filename: - dynamic[PLUGIN_NAME + 'save_dir'] = os.path.split(filename)[0] - with file(filename, 'w') as fname: + with file(filename, 'wb') as fname: if self.binary_file: fname.write(self.plugin_keys[keyname].decode('hex')) elif self.json_file: @@ -458,7 +464,7 @@ def __init__(self, parent=None,): self.resize(self.sizeHint()) def accept(self): - if self.key_ledit.text().isEmpty() or unicode(self.key_ledit.text()).isspace(): + if not unicode(self.key_ledit.text()) or unicode(self.key_ledit.text()).isspace(): errmsg = u"Key name field cannot be empty!" return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), _(errmsg), show=True, show_copy_button=False) @@ -479,7 +485,7 @@ def accept(self): @property def key_name(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() @@ -553,7 +559,7 @@ def __init__(self, parent=None,): @property def key_name(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() @property def key_value(self): @@ -562,11 +568,11 @@ def key_value(self): @property def user_name(self): - return unicode(self.name_ledit.text().toUtf8(), 'utf8').strip().lower().replace(' ','') + return unicode(self.name_ledit.text()).strip().lower().replace(' ','') @property def cc_number(self): - return unicode(self.cc_ledit.text().toUtf8(), 'utf8').strip().replace(' ', '').replace('-','') + return unicode(self.cc_ledit.text()).strip().replace(' ', '').replace('-','') def accept(self): @@ -634,7 +640,7 @@ def __init__(self, parent=None,): @property def key_name(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() @property def key_value(self): @@ -643,11 +649,11 @@ def key_value(self): @property def user_name(self): - return unicode(self.name_ledit.text().toUtf8(), 'utf8').strip().lower().replace(' ','') + return unicode(self.name_ledit.text()).strip().lower().replace(' ','') @property def cc_number(self): - return unicode(self.cc_ledit.text().toUtf8(), 'utf8').strip().replace(' ', '').replace('-','') + return unicode(self.cc_ledit.text()).strip().replace(' ', '').replace('-','') def accept(self): @@ -719,7 +725,7 @@ def __init__(self, parent=None,): @property def key_name(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() @property def key_value(self): @@ -792,7 +798,7 @@ def __init__(self, parent=None,): @property def key_name(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() @property def key_value(self): @@ -841,11 +847,11 @@ def __init__(self, parent=None,): @property def key_name(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() @property def key_value(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() def accept(self): if len(self.key_name) == 0 or self.key_name.isspace(): @@ -889,11 +895,11 @@ def __init__(self, parent=None,): @property def key_name(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() @property def key_value(self): - return unicode(self.key_ledit.text().toUtf8(), 'utf8').strip() + return unicode(self.key_ledit.text()).strip() def accept(self): if len(self.key_name) == 0 or self.key_name.isspace(): diff --git a/Other_Tools/Kindle_for_Android_Patches/kindle_version_4.0.2.1/kindle4.0.2.1.patch b/Other_Tools/Kindle_for_Android_Patches/kindle_version_4.0.2.1/kindle4.0.2.1.patch new file mode 100644 index 00000000..010f68f0 --- /dev/null +++ b/Other_Tools/Kindle_for_Android_Patches/kindle_version_4.0.2.1/kindle4.0.2.1.patch @@ -0,0 +1,238 @@ +Only in kindle4.0.2.1: build +diff -r -u10 kindle4.0.2.1_orig/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali kindle4.0.2.1/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali +--- kindle4.0.2.1_orig/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali 2013-05-22 18:39:03.000000000 -0500 ++++ kindle4.0.2.1/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali 2013-05-23 16:54:53.000000000 -0500 +@@ -36,20 +36,22 @@ + .field private maxCpuSpeed:J + + .field private maxMemory:J + + .field private minCpuSpeed:J + + .field private resources:Landroid/content/res/Resources; + + .field private security:Lcom/mobipocket/android/library/reader/AndroidSecurity; + ++.field private pidList:Ljava/lang/String; ++ + .field private totalMemory:J + + + # direct methods + .method static constructor ()V + .locals 1 + + .prologue + .line 30 + const-class v0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider; +@@ -72,20 +74,24 @@ + .prologue + .line 130 + invoke-direct {p0}, Ljava/lang/Object;->()V + + .line 131 + iput-object p1, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->security:Lcom/mobipocket/android/library/reader/AndroidSecurity; + + .line 132 + iput-object p2, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->deviceType:Lcom/amazon/kcp/application/AmazonDeviceType; + ++ const-string v0, "Open DRMed book to show PID list." ++ ++ iput-object v0, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->pidList:Ljava/lang/String; ++ + .line 133 + sget-object v0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->TAG:Ljava/lang/String; + + new-instance v0, Ljava/lang/StringBuilder; + + invoke-direct {v0}, Ljava/lang/StringBuilder;->()V + + const-string v1, "Device Type is set to \"" + + invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; +@@ -1235,10 +1241,33 @@ + move-result-wide v0 + + iput-wide v0, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->totalMemory:J + + .line 308 + :cond_0 + iget-wide v0, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->totalMemory:J + + return-wide v0 + .end method ++ ++.method public getPidList()Ljava/lang/String; ++ .locals 1 ++ ++ .prologue ++ .line 15 ++ iget-object v0, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->pidList:Ljava/lang/String; ++ ++ return-object v0 ++.end method ++ ++.method public setPidList(Ljava/lang/String;)V ++ .locals 0 ++ .parameter "value" ++ ++ .prologue ++ .line 11 ++ iput-object p1, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->pidList:Ljava/lang/String; ++ ++ .line 12 ++ return-void ++.end method ++ +diff -r -u10 kindle4.0.2.1_orig/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali kindle4.0.2.1/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali +--- kindle4.0.2.1_orig/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali 2013-05-22 18:39:03.000000000 -0500 ++++ kindle4.0.2.1/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali 2013-05-23 16:55:58.000000000 -0500 +@@ -23,10 +23,16 @@ + .end method + + .method public abstract getDeviceTypeId()Ljava/lang/String; + .end method + + .method public abstract getOsVersion()Ljava/lang/String; + .end method + + .method public abstract getPid()Ljava/lang/String; + .end method ++ ++.method public abstract getPidList()Ljava/lang/String; ++.end method ++ ++.method public abstract setPidList(Ljava/lang/String;)V ++.end method +diff -r -u10 kindle4.0.2.1_orig/smali/com/amazon/kcp/info/AboutActivity.smali kindle4.0.2.1/smali/com/amazon/kcp/info/AboutActivity.smali +--- kindle4.0.2.1_orig/smali/com/amazon/kcp/info/AboutActivity.smali 2013-05-22 18:39:03.000000000 -0500 ++++ kindle4.0.2.1/smali/com/amazon/kcp/info/AboutActivity.smali 2013-05-23 17:18:14.000000000 -0500 +@@ -486,20 +486,71 @@ + .end local v2 #screenDpi:Ljava/lang/String; + :cond_0 + iget-object v5, p0, Lcom/amazon/kcp/info/AboutActivity;->detailItemList:Ljava/util/List; + + invoke-interface {v5, v0}, Ljava/util/List;->add(Ljava/lang/Object;)Z + + .line 317 + return-void + .end method + ++.method private populatePIDList()V ++ .locals 7 ++ ++ .prologue ++ .line 313 ++ invoke-static {}, Lcom/amazon/kcp/application/DeviceInformationProviderFactory;->getProvider()Lcom/amazon/kcp/application/IDeviceInformationProvider; ++ ++ move-result-object v0 ++ ++ invoke-interface {v0}, Lcom/amazon/kcp/application/IDeviceInformationProvider;->getPidList()Ljava/lang/String; ++ ++ move-result-object v1 ++ ++ .line 314 ++ .local v1, PidList:Ljava/lang/String; ++ iget-object v3, p0, Lcom/amazon/kcp/info/AboutActivity;->groupItemList:Ljava/util/List; ++ ++ new-instance v4, Lcom/amazon/kcp/info/AboutActivity$GroupItem; ++ ++ const-string v5, "PID List" ++ ++ const v6, 0x1 ++ ++ invoke-direct {v4, p0, v5, v6}, Lcom/amazon/kcp/info/AboutActivity$GroupItem;->(Lcom/amazon/kcp/info/AboutActivity;Ljava/lang/String;Z)V ++ ++ invoke-interface {v3, v4}, Ljava/util/List;->add(Ljava/lang/Object;)Z ++ ++ .line 315 ++ new-instance v2, Ljava/util/ArrayList; ++ ++ invoke-direct {v2}, Ljava/util/ArrayList;->()V ++ ++ .line 316 ++ .local v2, children:Ljava/util/List;,"Ljava/util/List;" ++ new-instance v3, Lcom/amazon/kcp/info/AboutActivity$DetailItem; ++ ++ const-string v4, "PIDs" ++ ++ invoke-direct {v3, p0, v4, v1}, Lcom/amazon/kcp/info/AboutActivity$DetailItem;->(Lcom/amazon/kcp/info/AboutActivity;Ljava/lang/String;Ljava/lang/String;)V ++ ++ invoke-interface {v2, v3}, Ljava/util/List;->add(Ljava/lang/Object;)Z ++ ++ .line 317 ++ iget-object v3, p0, Lcom/amazon/kcp/info/AboutActivity;->detailItemList:Ljava/util/List; ++ ++ invoke-interface {v3, v2}, Ljava/util/List;->add(Ljava/lang/Object;)Z ++ ++ .line 318 ++ return-void ++.end method ++ + .method private populateDisplayItems()V + .locals 1 + + .prologue + .line 171 + iget-object v0, p0, Lcom/amazon/kcp/info/AboutActivity;->groupItemList:Ljava/util/List; + + if-nez v0, :cond_0 + + .line 173 +@@ -531,20 +582,22 @@ + + .line 192 + invoke-direct {p0}, Lcom/amazon/kcp/info/AboutActivity;->populateRamInformation()V + + .line 193 + invoke-direct {p0}, Lcom/amazon/kcp/info/AboutActivity;->populateStorageInformation()V + + .line 194 + invoke-direct {p0}, Lcom/amazon/kcp/info/AboutActivity;->populateDisplayInformation()V + ++ invoke-direct {p0}, Lcom/amazon/kcp/info/AboutActivity;->populatePIDList()V ++ + .line 195 + return-void + + .line 177 + :cond_0 + iget-object v0, p0, Lcom/amazon/kcp/info/AboutActivity;->groupItemList:Ljava/util/List; + + invoke-interface {v0}, Ljava/util/List;->clear()V + + goto :goto_0 +diff -r -u10 kindle4.0.2.1_orig/smali/com/amazon/system/security/Security.smali kindle4.0.2.1/smali/com/amazon/system/security/Security.smali +--- kindle4.0.2.1_orig/smali/com/amazon/system/security/Security.smali 2013-05-22 18:39:04.000000000 -0500 ++++ kindle4.0.2.1/smali/com/amazon/system/security/Security.smali 2013-05-23 17:19:05.000000000 -0500 +@@ -920,20 +920,30 @@ + + .line 350 + :cond_2 + add-int/lit8 v8, v8, 0x1 + + .line 351 + sget-object v0, Lcom/amazon/system/security/Security;->CUSTOM_PID_FOR_BUNDLED_DICTIONARY_DRM:Ljava/lang/String; + + aput-object v0, v6, v8 + ++ invoke-static {}, Lcom/amazon/kcp/application/DeviceInformationProviderFactory;->getProvider()Lcom/amazon/kcp/application/IDeviceInformationProvider; ++ ++ move-result-object v5 ++ ++ invoke-static {v6}, Ljava/util/Arrays;->toString([Ljava/lang/Object;)Ljava/lang/String; ++ ++ move-result-object v2 ++ ++ invoke-interface {v5, v2}, Lcom/amazon/kcp/application/IDeviceInformationProvider;->setPidList(Ljava/lang/String;)V ++ + .line 353 + return-object v6 + .end method + + + # virtual methods + .method public customDrmOnly()I + .locals 1 + + .prologue diff --git a/Other_Tools/Kobo/obok_2.01.py b/Other_Tools/Kobo/obok_2.01.py new file mode 100644 index 00000000..3ed7cbde --- /dev/null +++ b/Other_Tools/Kobo/obok_2.01.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python +# +# Updated September 2013 by Anon +# Version 2.01 +# Incorporated minor fixes posted at Apprentice Alf's. +# +# Updates July 2012 by Michael Newton +# PWSD ID is no longer a MAC address, but should always +# be stored in the registry. Script now works with OS X +# and checks plist for values instead of registry. Must +# have biplist installed for OS X support. +# +########################################################## +# KOBO DRM CRACK BY # +# PHYSISTICATED # +########################################################## +# This app was made for Python 2.7 on Windows 32-bit +# +# This app needs pycrypto - get from here: +# http://www.voidspace.org.uk/python/modules.shtml +# +# Usage: obok.py +# Choose the book you want to decrypt +# +# Shouts to my krew - you know who you are - and one in +# particular who gave me a lot of help with this - thank +# you so much! +# +# Kopimi /K\ +# Keep sharing, keep copying, but remember that nothing is +# for free - make sure you compensate your favorite +# authors - and cut out the middle man whenever possible +# ;) ;) ;) +# +# DRM AUTOPSY +# The Kobo DRM was incredibly easy to crack, but it took +# me months to get around to making this. Here's the +# basics of how it works: +# 1: Get MAC address of first NIC in ipconfig (sometimes +# stored in registry as pwsdid) +# 2: Get user ID (stored in tons of places, this gets it +# from HKEY_CURRENT_USER\Software\Kobo\Kobo Desktop +# Edition\Browser\cookies) +# 3: Concatenate and SHA256, take the second half - this +# is your master key +# 4: Open %LOCALAPPDATA%\Kobo Desktop Editions\Kobo.sqlite +# and dump content_keys +# 5: Unbase64 the keys, then decode these with the master +# key - these are your page keys +# 6: Unzip EPUB of your choice, decrypt each page with its +# page key, then zip back up again +# +# WHY USE THIS WHEN INEPT WORKS FINE? (adobe DRM stripper) +# Inept works very well, but authors on Kobo can choose +# what DRM they want to use - and some have chosen not to +# let people download them with Adobe Digital Editions - +# they would rather lock you into a single platform. +# +# With Obok, you can sync Kobo Desktop, decrypt all your +# ebooks, and then use them on whatever device you want +# - you bought them, you own them, you can do what you +# like with them. +# +# Obok is Kobo backwards, but it is also means "next to" +# in Polish. +# When you buy a real book, it is right next to you. You +# can read it at home, at work, on a train, you can lend +# it to a friend, you can scribble on it, and add your own +# explanations/translations. +# +# Obok gives you this power over your ebooks - no longer +# are you restricted to one device. This allows you to +# embed foreign fonts into your books, as older Kobo's +# can't display them properly. You can read your books +# on your phones, in different PC readers, and different +# ereader devices. You can share them with your friends +# too, if you like - you can do that with a real book +# after all. +# +""" +Decrypt Kobo encrypted EPUB books. +""" + +import os +import sys +if sys.platform.startswith('win'): + import _winreg +elif sys.platform.startswith('darwin'): + from biplist import readPlist +import re +import string +import hashlib +import sqlite3 +import base64 +import binascii +import zipfile +from Crypto.Cipher import AES + +def SHA256(raw): + return hashlib.sha256(raw).hexdigest() + +def RemoveAESPadding(contents): + lastchar = binascii.b2a_hex(contents[-1:]) + strlen = int(lastchar, 16) + padding = strlen + if(strlen == 1): + return contents[:-1] + if(strlen < 16): + for i in range(strlen): + testchar = binascii.b2a_hex(contents[-strlen:-(strlen-1)]) + if(testchar != lastchar): + padding = 0 + if(padding > 0): + contents = contents[:-padding] + return contents + +def GetVolumeKeys(dbase, enc): + volumekeys = {} + for row in dbase.execute("SELECT * from content_keys"): + if(row[0] not in volumekeys): + volumekeys[row[0]] = {} + volumekeys[row[0]][row[1]] = {} + volumekeys[row[0]][row[1]]["encryptedkey"] = base64.b64decode(row[2]) + volumekeys[row[0]][row[1]]["decryptedkey"] = enc.decrypt(volumekeys[row[0]][row[1]]["encryptedkey"]) + # get book name + for key in volumekeys.keys(): + volumekeys[key]["title"] = dbase.execute("SELECT Title from content where ContentID = '%s'" % (key)).fetchone()[0] + return volumekeys + +def ByteArrayToString(bytearr): + wincheck = re.match("@ByteArray\\((.+)\\)", bytearr) + if wincheck: + return wincheck.group(1) + return bytearr + +def GetUserHexKey(prefs = ""): + "find wsuid and pwsdid" + wsuid = "" + pwsdid = "" + if sys.platform.startswith('win'): + regkey_browser = _winreg.OpenKey(_winreg.HKEY_CURRENT_USER, "Software\\Kobo\\Kobo Desktop Edition\\Browser") + cookies = _winreg.QueryValueEx(regkey_browser, "cookies") + bytearrays = cookies[0] + elif sys.platform.startswith('darwin'): + cookies = readPlist(prefs) + bytearrays = cookies["Browser.cookies"] + for bytearr in bytearrays: + cookie = ByteArrayToString(bytearr) + print cookie + wsuidcheck = re.match("^wsuid=([0-9a-f-]+)", cookie) + if(wsuidcheck): + wsuid = wsuidcheck.group(1) + pwsdidcheck = re.match("^pwsdid=([0-9a-f-]+)", cookie) + if (pwsdidcheck): + pwsdid = pwsdidcheck.group(1) + + if(wsuid == "" or pwsdid == ""): + print "wsuid or pwsdid key not found :/" + exit() + preuserkey = string.join((pwsdid, wsuid), "") + print SHA256(pwsdid) + userkey = SHA256(preuserkey) + return userkey[32:] + +# get dirs +if sys.platform.startswith('win'): + delim = "\\" + if (sys.getwindowsversion().major > 5): + kobodir = string.join((os.environ['LOCALAPPDATA'], "Kobo\\Kobo Desktop Edition"), delim) + else: + kobodir = string.join((os.environ['USERPROFILE'], "Local Settings\\Application Data\\Kobo\\Kobo Desktop Edition"), delim) + prefs = "" +elif sys.platform.startswith('darwin'): + delim = "/" + kobodir = string.join((os.environ['HOME'], "Library/Application Support/Kobo/Kobo Desktop Edition"), delim) + prefs = string.join((os.environ['HOME'], "Library/Preferences/com.kobo.Kobo Desktop Edition.plist"), delim) +sqlitefile = string.join((kobodir, "Kobo.sqlite"), delim) +bookdir = string.join((kobodir, "kepub"), delim) + +# get key +userkeyhex = GetUserHexKey(prefs) +# load into AES +userkey = binascii.a2b_hex(userkeyhex) +enc = AES.new(userkey, AES.MODE_ECB) + +# open sqlite +conn = sqlite3.connect(sqlitefile) +dbcursor = conn.cursor() +# get volume keys +volumekeys = GetVolumeKeys(dbcursor, enc) + +# choose a volumeID + +volumeid = "" +print "Choose a book to decrypt:" +i = 1 +for key in volumekeys.keys(): + print "%d: %s" % (i, volumekeys[key]["title"]) + i += 1 + +num = input("...") + +i = 1 +for key in volumekeys.keys(): + if(i == num): + volumeid = key + i += 1 + +if(volumeid == ""): + exit() + +zippath = string.join((bookdir, volumeid), delim) + +z = zipfile.ZipFile(zippath, "r") +# make filename out of Unicode alphanumeric and whitespace equivalents from title +outname = "%s.epub" % (re.sub("[^\s\w]", "", volumekeys[volumeid]["title"], 0, re.UNICODE)) +zout = zipfile.ZipFile(outname, "w", zipfile.ZIP_DEFLATED) +for filename in z.namelist(): + #print filename + # read in and decrypt + if(filename in volumekeys[volumeid]): + # do decrypted version + pagekey = volumekeys[volumeid][filename]["decryptedkey"] + penc = AES.new(pagekey, AES.MODE_ECB) + contents = RemoveAESPadding(penc.decrypt(z.read(filename))) + # need to fix padding + zout.writestr(filename, contents) + else: + zout.writestr(filename, z.read(filename)) + +print "Book saved as %s%s%s" % (os.getcwd(), delim, outname) \ No newline at end of file