More work on standalone version, fix plugin

pull/24/head
NoDRM 2 years ago
parent 5ace15e912
commit a275d5d819

@ -54,3 +54,4 @@ List of changes since the fork of Apprentice Harper's repository:
- 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).

@ -17,7 +17,7 @@ p {margin-top: 0}
<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>

@ -2,13 +2,19 @@
#@@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@@

@ -8,7 +8,6 @@ from __future__ import print_function
# Copyright © 2021 NoDRM
__license__ = 'GPL v3'
__version__ = '10.0.2'
__docformat__ = 'restructuredtext en'
@ -88,12 +87,6 @@ __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
@ -101,6 +94,8 @@ import traceback
#@@CALIBRE_COMPAT_CODE@@
import __version
class DeDRMError(Exception):
pass
@ -147,6 +142,10 @@ class SafeUnbuffered:
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."

@ -0,0 +1,12 @@
#!/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'

@ -28,7 +28,7 @@ from calibre.constants import iswindows, isosx
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

@ -12,12 +12,20 @@ import traceback
#@@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

@ -9,10 +9,11 @@ from __future__ import absolute_import, print_function
OPT_SHORT_TO_LONG = [
["c", "config"],
["d", "dest"],
["e", "extract"],
["f", "force"],
["h", "help"],
["i", "import"],
["o", "output"],
["p", "password"],
["q", "quiet"],
["t", "test"],
@ -22,8 +23,6 @@ OPT_SHORT_TO_LONG = [
#@@CALIBRE_COMPAT_CODE@@
# Explicitly set the package identifier so we are allowed to import stuff ...
__package__ = "DeDRM_plugin"
import os, sys
@ -34,6 +33,9 @@ _additional_data = []
_additional_params = []
_function = None
global config_file_path
config_file_path = "dedrm.json"
def print_fname(f, info):
print(" " + f.ljust(15) + " " + info)
@ -64,7 +66,7 @@ def print_err_header():
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.")
@ -78,12 +80,13 @@ def print_help():
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.")
@ -105,18 +108,28 @@ def print_credits():
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)
@ -143,12 +156,28 @@ def handle_data(data):
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):
@ -236,7 +265,7 @@ 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)

@ -0,0 +1,140 @@
#!/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()

@ -18,18 +18,19 @@ iswindows = sys.platform.startswith('win')
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
@ -40,6 +41,7 @@ def perform_action(params, files):
return 0
extract = False
import_to_json = True
while len(params) > 0:
p = params.pop(0)
@ -52,21 +54,34 @@ def perform_action(params, files):
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
@ -92,11 +107,16 @@ def perform_action(params, files):
# 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!")
print("This code is not intended to be executed directly!", file=sys.stderr)

@ -0,0 +1,209 @@
#!/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)
Loading…
Cancel
Save