2011-06-16 11:59:20 +06:00
#!/usr/bin/env python
2012-12-19 19:48:11 +06:00
# -*- coding: utf-8 -*-
2011-06-16 11:59:20 +06:00
from __future__ import with_statement
2012-12-19 19:48:11 +06:00
__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)
2012-12-24 20:55:19 +06:00
# 0.4.11 - Fixed Linux support of K4PC
2012-12-25 05:31:34 +06:00
# 0.4.12 - More Linux Wine fixes
2012-12-19 19:48:11 +06:00
"""
Decrypt Amazon Kindle and Mobipocket encrypted ebooks .
"""
PLUGIN_NAME = u " Kindle and Mobipocket DeDRM "
2012-12-25 05:31:34 +06:00
PLUGIN_VERSION_TUPLE = ( 0 , 4 , 12 )
2012-12-19 19:48:11 +06:00
PLUGIN_VERSION = ' . ' . join ( [ str ( x ) for x in PLUGIN_VERSION_TUPLE ] )
import sys , os , re
import time
from zipfile import ZipFile
2011-06-16 11:59:20 +06:00
from calibre . customize import FileTypePlugin
2012-12-19 19:48:11 +06:00
from calibre . constants import iswindows , isosx
2011-06-16 11:59:20 +06:00
from calibre . gui2 import is_ok_to_use_qt
2012-03-07 00:24:28 +06:00
from calibre . utils . config import config_dir
2011-06-16 11:59:20 +06:00
2012-12-19 19:48:11 +06:00
# 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 )
2011-06-16 11:59:20 +06:00
class K4DeDRM ( FileTypePlugin ) :
2012-12-19 19:48:11 +06:00
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. "
2011-06-16 11:59:20 +06:00
supported_platforms = [ ' osx ' , ' windows ' , ' linux ' ] # Platforms this plugin will run on
2012-12-19 19:48:11 +06:00
author = u " DiapDealer, SomeUpdates, mdlnx, Apprentice Alf and The Dark Reverser "
version = PLUGIN_VERSION_TUPLE
2012-05-16 22:15:43 +06:00
file_types = set ( [ ' prc ' , ' mobi ' , ' azw ' , ' azw1 ' , ' azw3 ' , ' azw4 ' , ' tpz ' ] ) # The file types that this plugin will be applied to
2011-06-16 11:59:20 +06:00
on_import = True # Run this plugin during the import
2012-12-19 19:48:11 +06:00
priority = 521 # run this plugin before earlier versions
2011-06-16 11:59:20 +06:00
minimum_calibre_version = ( 0 , 7 , 55 )
2012-09-09 06:45:24 +06:00
2012-03-07 00:24:28 +06:00
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 :
2012-12-19 19:48:11 +06:00
names = [ u " alfcrypto.dll " , u " alfcrypto64.dll " ]
2012-03-07 00:24:28 +06:00
elif isosx :
2012-12-19 19:48:11 +06:00
names = [ u " libalfcrypto.dylib " ]
2012-03-07 00:24:28 +06:00
else :
2012-12-19 19:48:11 +06:00
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 " ]
2012-03-07 00:24:28 +06:00
lib_dict = self . load_resources ( names )
2012-12-19 19:48:11 +06:00
self . alfdir = os . path . join ( config_dir , u " alfcrypto " )
2012-03-07 00:24:28 +06:00
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 )
2012-12-19 19:48:11 +06:00
open ( file_path , ' wb ' ) . write ( data )
2011-06-16 11:59:20 +06:00
def run ( self , path_to_ebook ) :
2012-12-19 19:48:11 +06:00
# 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 ) )
2012-11-20 19:28:12 +06:00
# add the alfcrypto directory to sys.path so alfcrypto.py
2012-03-07 00:24:28 +06:00
# 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.
2012-12-19 19:48:11 +06:00
from calibre_plugins . k4mobidedrm import k4mobidedrm
2012-11-07 19:14:25 +06:00
2011-06-16 11:59:20 +06:00
k4 = True
pids = [ ]
serials = [ ]
kInfoFiles = [ ]
2012-11-20 19:28:12 +06:00
2012-09-09 06:45:24 +06:00
self . config ( )
2012-11-20 19:28:12 +06:00
2011-06-16 11:59:20 +06:00
# Get supplied list of PIDs to try from plugin customization.
2012-09-09 06:45:24 +06:00
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 :
2012-12-19 19:48:11 +06:00
print u " {0} v {1} : \' {2} \' is not a valid Mobipocket PID. " . format ( PLUGIN_NAME , PLUGIN_VERSION , pid )
2012-11-20 19:28:12 +06:00
2012-09-09 06:45:24 +06:00
# 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 ) )
2012-11-20 19:28:12 +06:00
2012-09-09 06:45:24 +06:00
# Get supplied list of Kindle serial numbers to try from plugin customization.
serialstringlistt = self . serials_string . split ( ' , ' )
for serial in serialstringlistt :
2012-11-20 19:28:12 +06:00
serial = str ( serial ) . replace ( " " , " " )
2012-12-19 19:48:11 +06:00
if len ( serial ) == 16 and serial [ 0 ] in [ ' B ' , ' 9 ' ] :
2012-09-09 06:45:24 +06:00
serials . append ( serial )
else :
if len ( serial ) > 0 :
2012-12-19 19:48:11 +06:00
print u " {0} v {1} : \' {2} \' is not a valid eInk Kindle serial number. " . format ( PLUGIN_NAME , PLUGIN_VERSION , serial )
2012-11-20 19:28:12 +06:00
2011-06-16 11:59:20 +06:00
# Load any kindle info files (*.info) included Calibre's config directory.
try :
2012-12-19 19:48:11 +06:00
print u " {0} v {1} : Calibre configuration directory is {2} " . format ( PLUGIN_NAME , PLUGIN_VERSION , config_dir )
2012-09-09 06:45:24 +06:00
files = os . listdir ( config_dir )
2011-06-16 11:59:20 +06:00
filefilter = re . compile ( " \ .info$| \ .kinf$ " , re . IGNORECASE )
files = filter ( filefilter . search , files )
if files :
for filename in files :
2012-09-09 06:45:24 +06:00
fpath = os . path . join ( config_dir , filename )
2011-06-16 11:59:20 +06:00
kInfoFiles . append ( fpath )
2012-12-19 19:48:11 +06:00
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 ] )
2011-06-16 11:59:20 +06:00
pass
try :
2012-12-19 19:48:11 +06:00
book = k4mobidedrm . GetDecryptedBook ( path_to_ebook , kInfoFiles , serials , pids , starttime )
except Exception , e :
2011-06-16 11:59:20 +06:00
#if you reached here then no luck raise and exception
if is_ok_to_use_qt ( ) :
2012-03-07 00:24:28 +06:00
from PyQt4 . Qt import QMessageBox
2012-12-19 19:48:11 +06:00
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 ) )
2012-03-07 00:24:28 +06:00
d . show ( )
d . raise_ ( )
d . exec_ ( )
2012-12-19 19:48:11 +06:00
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 ( )
2011-06-16 11:59:20 +06:00
return of . name
2012-09-09 06:45:24 +06:00
def WINEgetPIDs ( self , infile ) :
import subprocess
from subprocess import Popen , PIPE , STDOUT
import subasyncio
from subasyncio import Process
2012-12-19 19:48:11 +06:00
print u " Getting PIDs from Wine "
2012-09-09 06:45:24 +06:00
2012-12-19 19:48:11 +06:00
outfile = os . path . join ( self . alfdir + u " winepids.txt " )
2012-09-11 22:57:03 +06:00
# Remove any previous winepids.txt file.
if os . path . exists ( outfile ) :
os . remove ( outfile )
2012-09-09 06:45:24 +06:00
2012-12-19 19:48:11 +06:00
cmdline = u " wine python.exe \" {0} /getk4pcpids.py \" \" {1} \" \" {2} \" " . format ( self . alfdir , infile , outfile )
2012-09-09 06:45:24 +06:00
env = os . environ
2012-11-20 19:28:12 +06:00
2012-12-19 19:48:11 +06:00
print u " wine_prefix from tweaks is \' {0} \' " . format ( self . wine_prefix )
2012-09-09 06:45:24 +06:00
if ( " WINEPREFIX " in env ) :
2012-12-19 19:48:11 +06:00
print u " Using WINEPREFIX from the environment instead: \' {0} \' " . format ( env [ " WINEPREFIX " ] )
2012-09-09 06:45:24 +06:00
elif ( self . wine_prefix is not None ) :
2012-12-19 19:48:11 +06:00
env [ " WINEPREFIX " ] = self . wine_prefix
print u " Using WINEPREFIX from tweaks \' {0} \' " . format ( self . wine_prefix )
2012-09-09 06:45:24 +06:00
else :
2012-12-19 19:48:11 +06:00
print u " No wine prefix used. "
2012-09-09 06:45:24 +06:00
2012-12-19 19:48:11 +06:00
print u " Trying command: {0} " . format ( cmdline )
2012-09-09 06:45:24 +06:00
2012-09-11 22:57:03 +06:00
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 :
2012-12-19 19:48:11 +06:00
print u " WINE subprocess error: {0} " . format ( e . args [ 0 ] )
2012-09-11 22:57:03 +06:00
return [ ]
2012-12-19 19:48:11 +06:00
print u " WINE subprocess returned {0} " . format ( result )
2012-11-20 19:28:12 +06:00
2012-09-09 06:45:24 +06:00
WINEpids = [ ]
2012-09-11 22:57:03 +06:00
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 )
2012-12-19 19:48:11 +06:00
print u " Found PID ' {0} ' " . format ( customvalue )
2012-09-11 22:57:03 +06:00
else :
2012-12-19 19:48:11 +06:00
print u " ' {0} ' is not a valid PID. " . format ( customvalue )
2012-09-11 22:57:03 +06:00
except Exception , e :
2012-12-19 19:48:11 +06:00
print u " Error parsing winepids.txt: {0} " . format ( e . args [ 0 ] )
2012-09-11 22:57:03 +06:00
return [ ]
2012-12-19 19:48:11 +06:00
if len ( WINEpids ) == 0 :
print u " No PIDs generated by Wine Python subprocess. "
2012-09-09 06:45:24 +06:00
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 ( )
2012-11-20 19:28:12 +06:00
2012-09-09 06:45:24 +06:00
def config ( self ) :
from calibre_plugins . k4mobidedrm . config import prefs
2012-11-20 19:28:12 +06:00
2012-09-09 06:45:24 +06:00
self . pids_string = prefs [ ' pids ' ]
self . serials_string = prefs [ ' serials ' ]
self . wine_prefix = prefs [ ' WINEPREFIX ' ]
2012-11-20 19:28:12 +06:00
2012-09-09 06:45:24 +06:00
def save_settings ( self , config_widget ) :
'''
Save the settings specified by the user with config_widget .
'''
config_widget . save_settings ( )
self . config ( )
2012-03-07 00:24:28 +06:00
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 )
2012-11-20 19:28:12 +06:00
return ans