mirror of
https://github.com/noDRM/DeDRM_tools.git
synced 2024-11-05 21:46:10 +06:00
9fda194391
Plugins now include unaltered stand-alone scripts, so no longer need to keep separate copies.
266 lines
11 KiB
Python
266 lines
11 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
from __future__ import with_statement
|
|
__license__ = 'GPL v3'
|
|
__docformat__ = 'restructuredtext en'
|
|
|
|
|
|
# Released under the terms of the GNU General Public Licence, version 3
|
|
# <http://www.gnu.org/licenses/>
|
|
#
|
|
# Requires Calibre version 0.7.55 or higher.
|
|
#
|
|
# All credit given to The Dark Reverser for the original mobidedrm script.
|
|
# Thanks to all those who've worked on the scripts since 2008 to improve
|
|
# the support for formats and sources.
|
|
#
|
|
# Revision history:
|
|
# 0.4.8 - Major code change to use unaltered k4mobidedrm.py 4.8 and later
|
|
# 0.4.9 - typo fix
|
|
# 0.4.10 - Another Topaz Fix (class added to page and group and region)
|
|
|
|
"""
|
|
Decrypt Amazon Kindle and Mobipocket encrypted ebooks.
|
|
"""
|
|
|
|
PLUGIN_NAME = u"Kindle and Mobipocket DeDRM"
|
|
PLUGIN_VERSION_TUPLE = (0, 4, 10)
|
|
PLUGIN_VERSION = '.'.join([str(x) for x in PLUGIN_VERSION_TUPLE])
|
|
|
|
import sys, os, re
|
|
import time
|
|
from zipfile import ZipFile
|
|
|
|
from calibre.customize import FileTypePlugin
|
|
from calibre.constants import iswindows, isosx
|
|
from calibre.gui2 import is_ok_to_use_qt
|
|
from calibre.utils.config import config_dir
|
|
|
|
# Wrap a stream so that output gets flushed immediately
|
|
# and also make sure that any unicode strings get
|
|
# encoded using "replace" before writing them.
|
|
class SafeUnbuffered:
|
|
def __init__(self, stream):
|
|
self.stream = stream
|
|
self.encoding = stream.encoding
|
|
if self.encoding == None:
|
|
self.encoding = "utf-8"
|
|
def write(self, data):
|
|
if isinstance(data,unicode):
|
|
data = data.encode(self.encoding,"replace")
|
|
self.stream.write(data)
|
|
self.stream.flush()
|
|
def __getattr__(self, attr):
|
|
return getattr(self.stream, attr)
|
|
|
|
|
|
class K4DeDRM(FileTypePlugin):
|
|
name = PLUGIN_NAME
|
|
description = u"Removes DRM from eInk Kindle, Kindle 4 Mac and Kindle 4 PC ebooks, and from Mobipocket ebooks. Provided by the work of many including The Dark Reverser, DiapDealer, SomeUpdates, i♥cabbages, CMBDTC, Skindle, mdlnx, ApprenticeAlf, and probably others."
|
|
supported_platforms = ['osx', 'windows', 'linux'] # Platforms this plugin will run on
|
|
author = u"DiapDealer, SomeUpdates, mdlnx, Apprentice Alf and The Dark Reverser"
|
|
version = PLUGIN_VERSION_TUPLE
|
|
file_types = set(['prc','mobi','azw','azw1','azw3','azw4','tpz']) # The file types that this plugin will be applied to
|
|
on_import = True # Run this plugin during the import
|
|
priority = 521 # run this plugin before earlier versions
|
|
minimum_calibre_version = (0, 7, 55)
|
|
|
|
def initialize(self):
|
|
"""
|
|
Dynamic modules can't be imported/loaded from a zipfile... so this routine
|
|
runs whenever the plugin gets initialized. This will extract the appropriate
|
|
library for the target OS and copy it to the 'alfcrypto' subdirectory of
|
|
calibre's configuration directory. That 'alfcrypto' directory is then
|
|
inserted into the syspath (as the very first entry) in the run function
|
|
so the CDLL stuff will work in the alfcrypto.py script.
|
|
"""
|
|
if iswindows:
|
|
names = [u"alfcrypto.dll",u"alfcrypto64.dll"]
|
|
elif isosx:
|
|
names = [u"libalfcrypto.dylib"]
|
|
else:
|
|
names = [u"libalfcrypto32.so",u"libalfcrypto64.so",u"alfcrypto.py",u"alfcrypto.dll",u"alfcrypto64.dll",u"getk4pcpids.py",u"k4mobidedrm.py",u"mobidedrm.py",u"kgenpids.py",u"k4pcutils.py",u"topazextract.py"]
|
|
lib_dict = self.load_resources(names)
|
|
self.alfdir = os.path.join(config_dir,u"alfcrypto")
|
|
if not os.path.exists(self.alfdir):
|
|
os.mkdir(self.alfdir)
|
|
for entry, data in lib_dict.items():
|
|
file_path = os.path.join(self.alfdir, entry)
|
|
open(file_path,'wb').write(data)
|
|
|
|
def run(self, path_to_ebook):
|
|
# make sure any unicode output gets converted safely with 'replace'
|
|
sys.stdout=SafeUnbuffered(sys.stdout)
|
|
sys.stderr=SafeUnbuffered(sys.stderr)
|
|
|
|
starttime = time.time()
|
|
print u"{0} v{1}: Trying to decrypt {2}.".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook))
|
|
|
|
# add the alfcrypto directory to sys.path so alfcrypto.py
|
|
# will be able to locate the custom lib(s) for CDLL import.
|
|
sys.path.insert(0, self.alfdir)
|
|
# Had to move these imports here so the custom libs can be
|
|
# extracted to the appropriate places beforehand these routines
|
|
# look for them.
|
|
from calibre_plugins.k4mobidedrm import k4mobidedrm
|
|
|
|
k4 = True
|
|
pids = []
|
|
serials = []
|
|
kInfoFiles = []
|
|
|
|
self.config()
|
|
|
|
# Get supplied list of PIDs to try from plugin customization.
|
|
pidstringlistt = self.pids_string.split(',')
|
|
for pid in pidstringlistt:
|
|
pid = str(pid).strip()
|
|
if len(pid) == 10 or len(pid) == 8:
|
|
pids.append(pid)
|
|
else:
|
|
if len(pid) > 0:
|
|
print u"{0} v{1}: \'{2}\' is not a valid Mobipocket PID.".format(PLUGIN_NAME, PLUGIN_VERSION, pid)
|
|
|
|
# For linux, get PIDs by calling the right routines under WINE
|
|
if sys.platform.startswith('linux'):
|
|
k4 = False
|
|
pids.extend(self.WINEgetPIDs(path_to_ebook))
|
|
|
|
# Get supplied list of Kindle serial numbers to try from plugin customization.
|
|
serialstringlistt = self.serials_string.split(',')
|
|
for serial in serialstringlistt:
|
|
serial = str(serial).replace(" ","")
|
|
if len(serial) == 16 and serial[0] in ['B','9']:
|
|
serials.append(serial)
|
|
else:
|
|
if len(serial) > 0:
|
|
print u"{0} v{1}: \'{2}\' is not a valid eInk Kindle serial number.".format(PLUGIN_NAME, PLUGIN_VERSION, serial)
|
|
|
|
# Load any kindle info files (*.info) included Calibre's config directory.
|
|
try:
|
|
print u"{0} v{1}: Calibre configuration directory is {2}".format(PLUGIN_NAME, PLUGIN_VERSION, config_dir)
|
|
files = os.listdir(config_dir)
|
|
filefilter = re.compile("\.info$|\.kinf$", re.IGNORECASE)
|
|
files = filter(filefilter.search, files)
|
|
if files:
|
|
for filename in files:
|
|
fpath = os.path.join(config_dir, filename)
|
|
kInfoFiles.append(fpath)
|
|
print u"{0} v{1}: Kindle info/kinf file {2} found in config folder.".format(PLUGIN_NAME, PLUGIN_VERSION, filename)
|
|
except IOError, e:
|
|
print u"{0} v{1}: Error \'{2}\' reading kindle info/kinf files from config directory.".format(PLUGIN_NAME, PLUGIN_VERSION, e.args[0])
|
|
pass
|
|
|
|
try:
|
|
book = k4mobidedrm.GetDecryptedBook(path_to_ebook,kInfoFiles,serials,pids,starttime)
|
|
except Exception, e:
|
|
#if you reached here then no luck raise and exception
|
|
if is_ok_to_use_qt():
|
|
from PyQt4.Qt import QMessageBox
|
|
d = QMessageBox(QMessageBox.Warning, u"{0} v{1}".format(PLUGIN_NAME, PLUGIN_VERSION), u"Error after {1:.1f} seconds: {0}".format(e.args[0],time.time()-starttime))
|
|
d.show()
|
|
d.raise_()
|
|
d.exec_()
|
|
raise Exception(u"{0} v{1}: Error after {3:.1f} seconds: {2}".format(PLUGIN_NAME, PLUGIN_VERSION, e.args[0],time.time()-starttime))
|
|
|
|
|
|
print u"{0} v{1}: Successfully decrypted book after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-starttime)
|
|
|
|
of = self.temporary_file(k4mobidedrm.cleanup_name(k4mobidedrm.unescape(book.getBookTitle()))+book.getBookExtension())
|
|
book.getFile(of.name)
|
|
book.cleanup()
|
|
return of.name
|
|
|
|
def WINEgetPIDs(self, infile):
|
|
|
|
import subprocess
|
|
from subprocess import Popen, PIPE, STDOUT
|
|
|
|
import subasyncio
|
|
from subasyncio import Process
|
|
|
|
print u" Getting PIDs from Wine"
|
|
|
|
outfile = os.path.join(self.alfdir + u"winepids.txt")
|
|
# Remove any previous winepids.txt file.
|
|
if os.path.exists(outfile):
|
|
os.remove(outfile)
|
|
|
|
cmdline = u"wine python.exe \"{0}/getk4pcpids.py\" \"{1}\" \"{2}\"".format(self.alfdir,infile,outfile)
|
|
env = os.environ
|
|
|
|
print u"wine_prefix from tweaks is \'{0}\'".format(self.wine_prefix)
|
|
|
|
if ("WINEPREFIX" in env):
|
|
print u"Using WINEPREFIX from the environment instead: \'{0}\'".format(env["WINEPREFIX"])
|
|
elif (self.wine_prefix is not None):
|
|
env["WINEPREFIX"] = self.wine_prefix
|
|
print u"Using WINEPREFIX from tweaks \'{0}\'".format(self.wine_prefix)
|
|
else:
|
|
print u"No wine prefix used."
|
|
|
|
print u"Trying command: {0}".format(cmdline)
|
|
|
|
try:
|
|
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
|
p2 = Process(cmdline, shell=True, bufsize=1, stdin=None, stdout=sys.stdout, stderr=STDOUT, close_fds=False)
|
|
result = p2.wait("wait")
|
|
except Exception, e:
|
|
print u"WINE subprocess error: {0}".format(e.args[0])
|
|
return []
|
|
print u"WINE subprocess returned {0}".format(result)
|
|
|
|
WINEpids = []
|
|
if os.path.exists(outfile):
|
|
try:
|
|
customvalues = file(outfile, 'r').readline().split(',')
|
|
for customvalue in customvalues:
|
|
customvalue = str(customvalue)
|
|
customvalue = customvalue.strip()
|
|
if len(customvalue) == 10 or len(customvalue) == 8:
|
|
WINEpids.append(customvalue)
|
|
print u"Found PID '{0}'".format(customvalue)
|
|
else:
|
|
print u"'{0}' is not a valid PID.".format(customvalue)
|
|
except Exception, e:
|
|
print u"Error parsing winepids.txt: {0}".format(e.args[0])
|
|
return []
|
|
if len(WINEpids) == 0:
|
|
print u"No PIDs generated by Wine Python subprocess."
|
|
return WINEpids
|
|
|
|
def is_customizable(self):
|
|
# return true to allow customization via the Plugin->Preferences.
|
|
return True
|
|
|
|
def config_widget(self):
|
|
# It is important to put this import statement here rather than at the
|
|
# top of the module as importing the config class will also cause the
|
|
# GUI libraries to be loaded, which we do not want when using calibre
|
|
# from the command line
|
|
from calibre_plugins.k4mobidedrm.config import ConfigWidget
|
|
return config.ConfigWidget()
|
|
|
|
def config(self):
|
|
from calibre_plugins.k4mobidedrm.config import prefs
|
|
|
|
self.pids_string = prefs['pids']
|
|
self.serials_string = prefs['serials']
|
|
self.wine_prefix = prefs['WINEPREFIX']
|
|
|
|
def save_settings(self, config_widget):
|
|
'''
|
|
Save the settings specified by the user with config_widget.
|
|
'''
|
|
config_widget.save_settings()
|
|
self.config()
|
|
|
|
def load_resources(self, names):
|
|
ans = {}
|
|
with ZipFile(self.plugin_path, 'r') as zf:
|
|
for candidate in zf.namelist():
|
|
if candidate in names:
|
|
ans[candidate] = zf.read(candidate)
|
|
return ans
|