- ineptpdf: Support for V=5, R=5 and R=6 PDF files, and for AES256-encrypted PDFs.
- ineptpdf: Disable cross-reference streams in the output file. This may make PDFs slightly larger, but the current code for cross-reference streams seems to be buggy and sometimes creates corrupted PDFs.
- Drop support for importing key data from the ancient, pre "DeDRM" Calibre plugins ("Ignoble Epub DeDRM", "eReader PDB 2 PML" and "K4MobiDeDRM"). These are from 2011, I doubt anyone still has these installed, I can't even find a working link for these to test them. If you still have encryption keys in one of these plugins, you will need to update to DeDRM v10.0.2 or older (to convert the keys) before updating to DeDRM v10.0.3 or newer.
+- Some Python3 bugfixes for Amazon books (merged #10 by ableeker).
\ No newline at end of file
<body>
-<h1>DeDRM Plugin <span class="version">(v10.0.0)</span></h1>
+<h1>DeDRM Plugin <span class="version">(v10.0.2)</span></h1>
<p>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.</p>
#@@CALIBRE_COMPAT_CODE_START@@
import sys, os
-# Explicitly allow importing the parent folder
+# Explicitly allow importing everything ...
if os.path.dirname(os.path.dirname(os.path.abspath(__file__))) not in sys.path:
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+if os.path.dirname(os.path.abspath(__file__)) not in sys.path:
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
# Bugfix for Calibre < 5:
if "calibre" in sys.modules and sys.version_info[0] == 2:
from calibre.utils.config import config_dir
if os.path.join(config_dir, "plugins", "DeDRM.zip") not in sys.path:
sys.path.insert(0, os.path.join(config_dir, "plugins", "DeDRM.zip"))
+
+# Explicitly set the package identifier so we are allowed to import stuff ...
+#__package__ = "DeDRM_plugin"
+
#@@CALIBRE_COMPAT_CODE_END@@
# Copyright © 2021 NoDRM
__license__ = 'GPL v3'
-__version__ = '10.0.2'
__docformat__ = 'restructuredtext en'
Decrypt DRMed ebooks.
"""
-PLUGIN_NAME = "DeDRM"
-PLUGIN_VERSION_TUPLE = tuple([int(x) for x in __version__.split(".")])
-PLUGIN_VERSION = ".".join([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'
-
import codecs
import sys, os
import time
#@@CALIBRE_COMPAT_CODE@@
+import __version
+
class DeDRMError(Exception):
pass
def __getattr__(self, attr):
return getattr(self.stream, attr)
+PLUGIN_NAME = __version.PLUGIN_NAME
+PLUGIN_VERSION = __version.PLUGIN_VERSION
+PLUGIN_VERSION_TUPLE = __version.PLUGIN_VERSION_TUPLE
+
class DeDRM(FileTypePlugin):
name = PLUGIN_NAME
description = "Removes DRM from Amazon Kindle, Adobe Adept (including Kobo), Readium LCP, Barnes & Noble, Mobipocket and eReader ebooks. Credit given to i♥cabbages and The Dark Reverser for the original stand-alone scripts."
--- /dev/null
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+#@@CALIBRE_COMPAT_CODE@@
+
+PLUGIN_NAME = "DeDRM"
+__version__ = '10.0.2'
+
+PLUGIN_VERSION_TUPLE = tuple([int(x) for x in __version__.split(".")])
+PLUGIN_VERSION = ".".join([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'
\ No newline at end of file
from __init__ import PLUGIN_NAME, PLUGIN_VERSION
-from __init__ import RESOURCE_NAME as help_file_name
+from __version import RESOURCE_NAME as help_file_name
from utilities import uStrCmp
import prefs
#@@CALIBRE_COMPAT_CODE@@
-from calibre.utils.config import JSONConfig
+try:
+ from calibre.utils.config import JSONConfig
+except:
+ from standalone.jsonconfig import JSONConfig
+
from __init__ import PLUGIN_NAME
class DeDRM_Prefs():
- def __init__(self):
- JSON_PATH = os.path.join("plugins", PLUGIN_NAME.strip().lower().replace(' ', '_') + '.json')
+ def __init__(self, json_path=None):
+ if json_path is None:
+ JSON_PATH = os.path.join("plugins", PLUGIN_NAME.strip().lower().replace(' ', '_') + '.json')
+ else:
+ JSON_PATH = json_path
+
self.dedrmprefs = JSONConfig(JSON_PATH)
self.dedrmprefs.defaults['configured'] = False
OPT_SHORT_TO_LONG = [
["c", "config"],
- ["d", "dest"],
["e", "extract"],
["f", "force"],
["h", "help"],
+ ["i", "import"],
+ ["o", "output"],
["p", "password"],
["q", "quiet"],
["t", "test"],
#@@CALIBRE_COMPAT_CODE@@
-# Explicitly set the package identifier so we are allowed to import stuff ...
-__package__ = "DeDRM_plugin"
import os, sys
_additional_params = []
_function = None
+global config_file_path
+config_file_path = "dedrm.json"
+
def print_fname(f, info):
print(" " + f.ljust(15) + " " + info)
print()
def print_help():
- from __init__ import PLUGIN_NAME, PLUGIN_VERSION
+ from __version import PLUGIN_NAME, PLUGIN_VERSION
print(PLUGIN_NAME + " v" + PLUGIN_VERSION + " - DRM removal plugin by noDRM")
print("Based on DeDRM Calibre plugin by Apprentice Harper, Apprentice Alf and others.")
print("See https://github.com/noDRM/DeDRM_tools for more information.")
print()
print("Available functions:")
print_fname("passhash", "Manage Adobe PassHashes")
+ print_fname("remove_drm", "Remove DRM from one or multiple books")
print()
# TODO: All parameters that are global should be listed here.
def print_credits():
- from __init__ import PLUGIN_NAME, PLUGIN_VERSION
+ from __version import PLUGIN_NAME, PLUGIN_VERSION
print(PLUGIN_NAME + " v" + PLUGIN_VERSION + " - Calibre DRM removal plugin by noDRM")
print("Based on DeDRM Calibre plugin by Apprentice Harper, Apprentice Alf and others.")
print("See https://github.com/noDRM/DeDRM_tools for more information.")
def handle_single_argument(arg, next):
used_up = 0
global _additional_params
+ global config_file_path
- if arg in ["--username", "--password"]:
+ if arg in ["--username", "--password", "--output", "--outputdir"]:
used_up = 1
_additional_params.append(arg)
- if next is None:
+ if next is None or len(next) == 0:
print_err_header()
print("Missing parameter for argument " + arg, file=sys.stderr)
sys.exit(1)
else:
_additional_params.append(next[0])
+
+ elif arg == "--config":
+ if next is None or len(next) == 0:
+ print_err_header()
+ print("Missing parameter for argument " + arg, file=sys.stderr)
+ sys.exit(1)
+
+ config_file_path = next[0]
+ used_up = 1
- elif arg in ["--help", "--credits", "--verbose", "--quiet", "--extract"]:
+ elif arg in ["--help", "--credits", "--verbose", "--quiet", "--extract", "--import", "--overwrite", "--force"]:
_additional_params.append(arg)
def execute_action(action, filenames, params):
print("Executing '{0}' on file(s) {1} with parameters {2}".format(action, str(filenames), str(params)), file=sys.stderr)
- if action == "passhash":
+ if action == "help":
+ print_help()
+ sys.exit(0)
+
+ elif action == "passhash":
from standalone.passhash import perform_action
perform_action(params, filenames)
+
+ elif action == "remove_drm":
+ if not os.path.isfile(os.path.abspath(config_file_path)):
+ print("Config file missing ...")
+
+ from standalone.remove_drm import perform_action
+ perform_action(params, filenames)
+
+ elif action == "config":
+ import prefs
+ config = prefs.DeDRM_Prefs(os.path.abspath(config_file_path))
+ print(config["adeptkeys"])
else:
- print("ERROR: This feature is still in development. Right now it can't be used yet.", file=sys.stderr)
+ print("Command '"+action+"' is unknown.", file=sys.stderr)
def main(argv):
# This function gets told what to do and gets additional data (filenames).
# It also receives additional parameters.
# The rest of the code will be in different Python files.
- execute_action(_function, _additional_data, _additional_params)
+ execute_action(_function.lower(), _additional_data, _additional_params)
--- /dev/null
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+# CLI interface for the DeDRM plugin (useable without Calibre, too)
+# Config implementation
+
+from __future__ import absolute_import, print_function
+
+# Taken from Calibre code - Copyright © 2008, Kovid Goyal kovid@kovidgoyal.net, GPLv3
+
+#@@CALIBRE_COMPAT_CODE@@
+
+import sys, os, codecs, json
+
+config_dir = "/"
+CONFIG_DIR_MODE = 0o700
+iswindows = sys.platform.startswith('win')
+
+
+filesystem_encoding = sys.getfilesystemencoding()
+if filesystem_encoding is None:
+ filesystem_encoding = 'utf-8'
+else:
+ try:
+ if codecs.lookup(filesystem_encoding).name == 'ascii':
+ filesystem_encoding = 'utf-8'
+ # On linux, unicode arguments to os file functions are coerced to an ascii
+ # bytestring if sys.getfilesystemencoding() == 'ascii', which is
+ # just plain dumb. This is fixed by the icu.py module which, when
+ # imported changes ascii to utf-8
+ except Exception:
+ filesystem_encoding = 'utf-8'
+
+
+class JSONConfig(dict):
+
+ EXTENSION = '.json'
+
+
+ def __init__(self, rel_path_to_cf_file, base_path=config_dir):
+ dict.__init__(self)
+ self.no_commit = False
+ self.defaults = {}
+ self.file_path = os.path.join(base_path,
+ *(rel_path_to_cf_file.split('/')))
+ self.file_path = os.path.abspath(self.file_path)
+ if not self.file_path.endswith(self.EXTENSION):
+ self.file_path += self.EXTENSION
+
+ self.refresh()
+
+ def mtime(self):
+ try:
+ return os.path.getmtime(self.file_path)
+ except OSError:
+ return 0
+
+ def touch(self):
+ try:
+ os.utime(self.file_path, None)
+ except OSError:
+ pass
+
+
+ def decouple(self, prefix):
+ self.file_path = os.path.join(os.path.dirname(self.file_path), prefix + os.path.basename(self.file_path))
+ self.refresh()
+
+ def refresh(self, clear_current=True):
+ d = {}
+ if os.path.exists(self.file_path):
+ with open(self.file_path, "rb") as f:
+ raw = f.read()
+ try:
+ d = self.raw_to_object(raw) if raw.strip() else {}
+ except SystemError:
+ pass
+ except:
+ import traceback
+ traceback.print_exc()
+ d = {}
+ if clear_current:
+ self.clear()
+ self.update(d)
+
+ def has_key(self, key):
+ return dict.__contains__(self, key)
+
+ def set(self, key, val):
+ self.__setitem__(key, val)
+
+ def __delitem__(self, key):
+ try:
+ dict.__delitem__(self, key)
+ except KeyError:
+ pass # ignore missing keys
+ else:
+ self.commit()
+
+ def commit(self):
+ if self.no_commit:
+ return
+ if hasattr(self, 'file_path') and self.file_path:
+ dpath = os.path.dirname(self.file_path)
+ if not os.path.exists(dpath):
+ os.makedirs(dpath, mode=CONFIG_DIR_MODE)
+ with open(self.file_path, "w") as f:
+ raw = self.to_raw()
+ f.seek(0)
+ f.truncate()
+ f.write(raw)
+
+ def __enter__(self):
+ self.no_commit = True
+
+ def __exit__(self, *args):
+ self.no_commit = False
+ self.commit()
+
+ def raw_to_object(self, raw):
+ return json.loads(raw)
+
+ def to_raw(self):
+ return json.dumps(self, ensure_ascii=False)
+
+ def __getitem__(self, key):
+ try:
+ return dict.__getitem__(self, key)
+ except KeyError:
+ return self.defaults[key]
+
+ def get(self, key, default=None):
+ try:
+ return dict.__getitem__(self, key)
+ except KeyError:
+ return self.defaults.get(key, default)
+
+ def __setitem__(self, key, val):
+ dict.__setitem__(self, key, val)
+ self.commit()
\ No newline at end of file
isosx = sys.platform.startswith('darwin')
def print_passhash_help():
- from __init__ import PLUGIN_NAME, PLUGIN_VERSION
+ from __version import PLUGIN_NAME, PLUGIN_VERSION
print(PLUGIN_NAME + " v" + PLUGIN_VERSION + " - Calibre DRM removal plugin by noDRM")
print()
print("passhash: Manage Adobe PassHashes")
print()
- print_std_usage("passhash", "[ -u username -p password | -e ]")
+ print_std_usage("passhash", "[ -u username -p password | -b base64str ] [ -i ] ")
print()
print("Options: ")
print_opt("u", "username", "Generate a PassHash with the given username")
- print_opt("p", "password", "Generate a PassHash with the given username")
- print_opt("e", "extract", "Extract PassHashes found on this machine")
+ print_opt("p", "password", "Generate a PassHash with the given password")
+ print_opt("e", "extract", "Display PassHashes found on this machine")
+ print_opt("i", "import", "Import hashes into the JSON config file")
def perform_action(params, files):
user = None
return 0
extract = False
+ import_to_json = True
while len(params) > 0:
p = params.pop(0)
elif p == "--help":
print_passhash_help()
return 0
+ elif p == "--import":
+ import_to_json = True
- if not extract:
+ if not extract and not import_to_json:
if user is None:
print("Missing parameter: --username", file=sys.stderr)
if pwd is None:
print("Missing parameter: --password", file=sys.stderr)
if user is None or pwd is None:
return 1
+
+ if user is None and pwd is not None:
+ print("Parameter --password also requires --username", file=sys.stderr)
+ return 1
+ if user is not None and pwd is None:
+ print("Parameter --username also requires --password", file=sys.stderr)
+ return 1
if user is not None and pwd is not None:
from ignoblekeyGenPassHash import generate_key
key = generate_key(user, pwd)
+ if import_to_json:
+ # TODO: Import the key to the JSON
+ pass
+
print(key.decode("utf-8"))
- if extract:
+ if extract or import_to_json:
if not iswindows and not isosx:
print("Extracting PassHash keys not supported on Linux.", file=sys.stderr)
return 1
# Print all found keys
for k in newkeys:
- print(k)
+ if import_to_json:
+ # TODO: Add keys to json
+ pass
+
+ if extract:
+ print(k)
return 0
if __name__ == "__main__":
- print("This code is not intended to be executed directly!")
\ No newline at end of file
+ print("This code is not intended to be executed directly!", file=sys.stderr)
\ No newline at end of file
--- /dev/null
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+# CLI interface for the DeDRM plugin (useable without Calibre, too)
+# DRM removal
+
+from __future__ import absolute_import, print_function
+
+# Copyright © 2021 NoDRM
+
+#@@CALIBRE_COMPAT_CODE@@
+
+import os, sys
+
+from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED
+from contextlib import closing
+
+from standalone.__init__ import print_opt, print_std_usage
+
+iswindows = sys.platform.startswith('win')
+isosx = sys.platform.startswith('darwin')
+
+def print_removedrm_help():
+ from __init__ import PLUGIN_NAME, PLUGIN_VERSION
+ print(PLUGIN_NAME + " v" + PLUGIN_VERSION + " - Calibre DRM removal plugin by noDRM")
+ print()
+ print("remove_drm: Remove DRM from one or multiple files")
+ print()
+ print_std_usage("remove_drm", "<filename> ... [ -o <filename> ] [ -f ]")
+
+ print()
+ print("Options: ")
+ print_opt(None, "outputdir", "Folder to export the file(s) to")
+ print_opt("o", "output", "File name to export the file to")
+ print_opt("f", "force", "Overwrite output file if it already exists")
+ print_opt(None, "overwrite", "Replace DRMed file with DRM-free file (implies --force)")
+
+
+def determine_file_type(file):
+ # Returns a file type:
+ # "PDF", "PDB", "MOBI", "TPZ", "LCP", "ADEPT", "ADEPT-PassHash", "KFX-ZIP", "ZIP" or None
+
+ f = open(file, "rb")
+ fdata = f.read(100)
+ f.close()
+
+ if fdata.startswith(b"PK\x03\x04"):
+ pass
+ # Either LCP, Adobe, or Amazon
+ elif fdata.startswith(b"%PDF"):
+ return "PDF"
+ elif fdata[0x3c:0x3c+8] == b"PNRdPPrs" or fdata[0x3c:0x3c+8] == b"PDctPPrs":
+ return "PDB"
+ elif fdata[0x3c:0x3c+8] == b"BOOKMOBI" or fdata[0x3c:0x3c+8] == b"TEXtREAd":
+ return "MOBI"
+ elif fdata.startswith(b"TPZ"):
+ return "TPZ"
+ else:
+ return None
+ # Unknown file type
+
+
+ # If it's a ZIP, determine the type.
+
+ from lcpdedrm import isLCPbook
+ if isLCPbook(file):
+ return "LCP"
+
+ from ineptepub import adeptBook, isPassHashBook
+ if adeptBook(file):
+ if isPassHashBook(file):
+ return "ADEPT-PassHash"
+ else:
+ return "ADEPT"
+
+ try:
+ # Amazon / KFX-ZIP has a file that starts with b'\xeaDRMION\xee' in the ZIP.
+ with closing(ZipFile(open(file, "rb"))) as book:
+ for subfilename in book.namelist():
+ with book.open(subfilename) as subfile:
+ data = subfile.read(8)
+ if data == b'\xeaDRMION\xee':
+ return "KFX-ZIP"
+ except:
+ pass
+
+ return "ZIP"
+
+
+
+
+def dedrm_single_file(input_file, output_file):
+ # When this runs, all the stupid file handling is done.
+ # Just take the file at the absolute path "input_file"
+ # and export it, DRM-free, to "output_file".
+
+ # Use a temp file as input_file and output_file
+ # might be identical.
+
+ # The output directory might not exist yet.
+
+ print("File " + input_file + " to " + output_file)
+
+ # Okay, first check the file type and don't rely on the extension.
+ try:
+ ftype = determine_file_type(input_file)
+ except:
+ print("Can't determine file type for this file.")
+ ftype = None
+
+ if ftype is None:
+ return
+
+
+
+
+
+def perform_action(params, files):
+ output = None
+ outputdir = None
+ force = False
+ overwrite_original = False
+
+
+ if len(files) == 0:
+ print_removedrm_help()
+ return 0
+
+ while len(params) > 0:
+ p = params.pop(0)
+ if p == "--output":
+ output = params.pop(0)
+ elif p == "--outputdir":
+ outputdir = params.pop(0)
+ elif p == "--force":
+ force = True
+ elif p == "--overwrite":
+ overwrite_original = True
+ force = True
+ elif p == "--help":
+ print_removedrm_help()
+ return 0
+
+ if overwrite_original and (output is not None or outputdir is not None):
+ print("Can't use --overwrite together with --output or --outputdir.")
+ return 1
+
+ if output is not None and os.path.isfile(output) and not force:
+ print("Output file already exists. Use --force to overwrite.", file=sys.stderr)
+ return 1
+
+
+ if output is not None and len(files) > 1:
+ print("Cannot set output file name if there's multiple input files.", file=sys.stderr)
+ return 1
+
+ if outputdir is not None and output is not None and os.path.isabs(output):
+ print("--output parameter is absolute path despite --outputdir being set.")
+ print("Remove --outputdir, or give a relative path to --output.")
+ return 1
+
+
+
+ for file in files:
+
+ file = os.path.abspath(file)
+
+ if not os.path.isfile(file):
+ print("Skipping file " + file + " - not found.")
+ continue
+
+ if overwrite_original:
+ output_filename = file
+ else:
+ if output is not None:
+ # Due to the check above, we DO only have one file here.
+ if outputdir is not None and not os.path.isabs(output):
+ output_filename = os.path.join(outputdir, output)
+ else:
+ output_filename = os.path.abspath(output)
+ else:
+ if outputdir is None:
+ outputdir = os.getcwd()
+ output_filename = os.path.join(outputdir, os.path.basename(file))
+ output_filename = os.path.abspath(output_filename)
+
+ if output_filename == file:
+ # If we export to the import folder, add a suffix to the file name.
+ fn, f_ext = os.path.splitext(output_filename)
+ output_filename = fn + "_nodrm" + f_ext
+
+
+
+ if os.path.isfile(output_filename) and not force:
+ print("Skipping file " + file + " because output file already exists (use --force).", file=sys.stderr)
+ continue
+
+
+
+ dedrm_single_file(file, output_filename)
+
+
+
+
+ return 0
+
+
+if __name__ == "__main__":
+ print("This code is not intended to be executed directly!", file=sys.stderr)
\ No newline at end of file