#!/usr/bin/env python # -*- coding: utf-8 -*- # DeDRM.pyw # Copyright 2010-2019 some_updates, Apprentice Alf and Apprentice Harper # Revision history: # 6.0.0 - Release along with unified plugin # 6.0.1 - Bug Fixes for Windows App # 6.0.2 - Fixed problem with spaces in paths and the bat file # 6.0.3 - Fix for Windows non-ascii user names # 6.0.4 - Fix for other potential unicode problems # 6.0.5 - Fix typo # 6.2.0 - Update to match plugin and AppleScript # 6.2.1 - Fix for non-ascii user names # 6.2.2 - Added URL method for B&N/nook books # 6.3.0 - Add in Android support # 6.3.1 - Version bump for clarity # 6.3.2 - Version bump to match plugin # 6.3.3 - Version bump to match plugin # 6.3.4 - Version bump to match plugin # 6.3.5 - Version bump to match plugin # 6.3.6 - Version bump to match plugin # 6.4.0 - Fix for Kindle for PC encryption change # 6.4.1 - Fix for new tags in Topaz ebooks # 6.4.2 - Fix for new tags in Topaz ebooks, and very small Topaz ebooks # 6.4.3 - Version bump to match plugin & Mac app # 6.5.0 - Fix for some new tags in Topaz ebooks # 6.5.1 - Version bump to match plugin & Mac app # 6.5.2 - Fix for a new tag in Topaz ebooks # 6.5.3 - Explicitly warn about KFX files # 6.5.4 - PDF float fix. # 6.5.5 - Kindle for PC/Accented characters in username fix. # 6.6.0 - Initial KFX support from TomThumb # 6.6.1 - Standalong app fix from wzyboy # 6.6.2 - Version bump for 64-bit Mac OS X app and various fixes. # 6.6.3 - Version bump for Kindle book name fixes and start of support for .kinf2018 __version__ = '6.6.3' import sys import os, os.path sys.path.append(os.path.join(sys.path[0],"lib")) import sys, os import codecs from argv_utils import add_cp65001_codec, set_utf8_default_encoding, unicode_argv add_cp65001_codec() set_utf8_default_encoding() import shutil import Tkinter from Tkinter import * import Tkconstants import tkFileDialog from scrolltextwidget import ScrolledText from activitybar import ActivityBar if sys.platform.startswith("win"): from askfolder_ed import AskFolder import re import simpleprefs import traceback from Queue import Full from Queue import Empty from multiprocessing import Process, Queue from scriptinterface import decryptepub, decryptpdb, decryptpdf, decryptk4mobi # Wrap a stream so that output gets flushed immediately # and appended to shared queue class QueuedUTF8Stream: def __init__(self, stream, q): self.stream = stream self.encoding = 'utf-8' self.q = q def write(self, data): if isinstance(data,unicode): data = data.encode('utf-8',"replace") self.q.put(data) def __getattr__(self, attr): return getattr(self.stream, attr) class DrmException(Exception): pass class MainApp(Tk): def __init__(self, apphome, dnd=False, filenames=[]): Tk.__init__(self) self.withdraw() self.dnd = dnd self.apphome = apphome # preference settings # [dictionary key, file in preferences directory where info is stored] description = [ ['pids' , 'pidlist.txt' ], ['serials', 'seriallist.txt'], ['sdrms' , 'sdrmlist.txt' ], ['outdir' , 'outdir.txt' ]] self.po = simpleprefs.SimplePrefs("DeDRM",description) if self.dnd: self.cd = ConvDialog(self) prefs = self.getPreferences() self.cd.doit(prefs, filenames) else: prefs = self.getPreferences() self.pd = PrefsDialog(self, prefs) self.cd = ConvDialog(self) self.pd.show() def getPreferences(self): prefs = self.po.getPreferences() prefdir = prefs['dir'] adeptkeyfile = os.path.join(prefdir,'adeptkey.der') if not os.path.exists(adeptkeyfile): import adobekey try: adobekey.getkey(adeptkeyfile) except: pass kindlekeyfile = os.path.join(prefdir,'kindlekey.k4i') if not os.path.exists(kindlekeyfile): import kindlekey try: kindlekey.getkey(kindlekeyfile) except: traceback.print_exc() pass bnepubkeyfile = os.path.join(prefdir,'bnepubkey.b64') if not os.path.exists(bnepubkeyfile): import ignoblekey try: ignoblekey.getkey(bnepubkeyfile) except: traceback.print_exc() pass return prefs def setPreferences(self, newprefs): prefdir = self.po.prefdir if 'adkfile' in newprefs: dfile = newprefs['adkfile'] fname = os.path.basename(dfile) nfile = os.path.join(prefdir,fname) if os.path.isfile(dfile): shutil.copyfile(dfile,nfile) if 'bnkfile' in newprefs: dfile = newprefs['bnkfile'] fname = os.path.basename(dfile) nfile = os.path.join(prefdir,fname) if os.path.isfile(dfile): shutil.copyfile(dfile,nfile) if 'kindlefile' in newprefs: dfile = newprefs['kindlefile'] fname = os.path.basename(dfile) nfile = os.path.join(prefdir,fname) if os.path.isfile(dfile): shutil.copyfile(dfile,nfile) if 'androidfile' in newprefs: dfile = newprefs['androidfile'] fname = os.path.basename(dfile) nfile = os.path.join(prefdir,fname) if os.path.isfile(dfile): shutil.copyfile(dfile,nfile) self.po.setPreferences(newprefs) return def alldone(self): if not self.dnd: self.pd.enablebuttons() else: self.destroy() class PrefsDialog(Toplevel): def __init__(self, mainapp, prefs_array): Toplevel.__init__(self, mainapp) self.withdraw() self.protocol("WM_DELETE_WINDOW", self.withdraw) self.title("DeDRM " + __version__) self.prefs_array = prefs_array self.status = Tkinter.Label(self, text='Setting Preferences') self.status.pack(fill=Tkconstants.X, expand=1) body = Tkinter.Frame(self) self.body = body body.pack(fill=Tkconstants.X, expand=1) sticky = Tkconstants.E + Tkconstants.W body.grid_columnconfigure(1, weight=2) cur_row = 0 Tkinter.Label(body, text='Adobe Key file (adeptkey.der)').grid(row=cur_row, sticky=Tkconstants.E) self.adkpath = Tkinter.Entry(body, width=50) self.adkpath.grid(row=cur_row, column=1, sticky=sticky) prefdir = self.prefs_array['dir'] keyfile = os.path.join(prefdir,'adeptkey.der') if os.path.isfile(keyfile): path = keyfile self.adkpath.insert(cur_row, path) button = Tkinter.Button(body, text="...", command=self.get_adkpath) button.grid(row=cur_row, column=2) cur_row = cur_row + 1 Tkinter.Label(body, text='Kindle Key file (kindlekey.k4i)').grid(row=cur_row, sticky=Tkconstants.E) self.kkpath = Tkinter.Entry(body, width=50) self.kkpath.grid(row=cur_row, column=1, sticky=sticky) prefdir = self.prefs_array['dir'] keyfile = os.path.join(prefdir,'kindlekey.k4i') if os.path.isfile(keyfile): path = keyfile self.kkpath.insert(0, path) button = Tkinter.Button(body, text="...", command=self.get_kkpath) button.grid(row=cur_row, column=2) cur_row = cur_row + 1 Tkinter.Label(body, text='Android Kindle backup file (backup.ab)').grid(row=cur_row, sticky=Tkconstants.E) self.akkpath = Tkinter.Entry(body, width=50) self.akkpath.grid(row=cur_row, column=1, sticky=sticky) prefdir = self.prefs_array['dir'] keyfile = os.path.join(prefdir,'backup.ab') if os.path.isfile(keyfile): path = keyfile self.akkpath.insert(0, path) button = Tkinter.Button(body, text="...", command=self.get_akkpath) button.grid(row=cur_row, column=2) cur_row = cur_row + 1 Tkinter.Label(body, text='Barnes and Noble Key file (bnepubkey.b64)').grid(row=cur_row, sticky=Tkconstants.E) self.bnkpath = Tkinter.Entry(body, width=50) self.bnkpath.grid(row=cur_row, column=1, sticky=sticky) prefdir = self.prefs_array['dir'] keyfile = os.path.join(prefdir,'bnepubkey.b64') if os.path.isfile(keyfile): path = keyfile self.bnkpath.insert(0, path) button = Tkinter.Button(body, text="...", command=self.get_bnkpath) button.grid(row=cur_row, column=2) cur_row = cur_row + 1 Tkinter.Label(body, text='Mobipocket PID list\n(8 or 10 characters, comma separated)').grid(row=cur_row, sticky=Tkconstants.E) self.pidnums = Tkinter.StringVar() self.pidinfo = Tkinter.Entry(body, width=50, textvariable=self.pidnums) if 'pids' in self.prefs_array: self.pidnums.set(self.prefs_array['pids']) self.pidinfo.grid(row=cur_row, column=1, sticky=sticky) cur_row = cur_row + 1 Tkinter.Label(body, text='eInk Kindle Serial Number list\n(16 characters, comma separated)').grid(row=cur_row, sticky=Tkconstants.E) self.sernums = Tkinter.StringVar() self.serinfo = Tkinter.Entry(body, width=50, textvariable=self.sernums) if 'serials' in self.prefs_array: self.sernums.set(self.prefs_array['serials']) self.serinfo.grid(row=cur_row, column=1, sticky=sticky) cur_row = cur_row + 1 Tkinter.Label(body, text='eReader data list\n(name:last 8 digits on credit card, comma separated)').grid(row=cur_row, sticky=Tkconstants.E) self.sdrmnums = Tkinter.StringVar() self.sdrminfo = Tkinter.Entry(body, width=50, textvariable=self.sdrmnums) if 'sdrms' in self.prefs_array: self.sdrmnums.set(self.prefs_array['sdrms']) self.sdrminfo.grid(row=cur_row, column=1, sticky=sticky) cur_row = cur_row + 1 Tkinter.Label(body, text="Output Folder (if blank, use input ebook's folder)").grid(row=cur_row, sticky=Tkconstants.E) self.outpath = Tkinter.Entry(body, width=50) self.outpath.grid(row=cur_row, column=1, sticky=sticky) if 'outdir' in self.prefs_array: dpath = self.prefs_array['outdir'] self.outpath.insert(0, dpath) button = Tkinter.Button(body, text="...", command=self.get_outpath) button.grid(row=cur_row, column=2) cur_row = cur_row + 1 Tkinter.Label(body, text='').grid(row=cur_row, column=0, columnspan=2, sticky=Tkconstants.N) cur_row = cur_row + 1 Tkinter.Label(body, text='Alternatively Process an eBook').grid(row=cur_row, column=0, columnspan=2, sticky=Tkconstants.N) cur_row = cur_row + 1 Tkinter.Label(body, text='Select an eBook to Process*').grid(row=cur_row, sticky=Tkconstants.E) self.bookpath = Tkinter.Entry(body, width=50) self.bookpath.grid(row=cur_row, column=1, sticky=sticky) button = Tkinter.Button(body, text="...", command=self.get_bookpath) button.grid(row=cur_row, column=2) cur_row = cur_row + 1 Tkinter.Label(body, font=("Helvetica", "10", "italic"), text='*To DeDRM multiple ebooks simultaneously, set your preferences and quit.\nThen drag and drop ebooks or folders onto the DeDRM_Drop_Target').grid(row=cur_row, column=1, sticky=Tkconstants.E) cur_row = cur_row + 1 Tkinter.Label(body, text='').grid(row=cur_row, column=0, columnspan=2, sticky=Tkconstants.E) buttons = Tkinter.Frame(self) buttons.pack() self.sbotton = Tkinter.Button(buttons, text="Set Prefs", width=14, command=self.setprefs) self.sbotton.pack(side=Tkconstants.LEFT) buttons.pack() self.pbotton = Tkinter.Button(buttons, text="Process eBook", width=14, command=self.doit) self.pbotton.pack(side=Tkconstants.LEFT) buttons.pack() self.qbotton = Tkinter.Button(buttons, text="Quit", width=14, command=self.quitting) self.qbotton.pack(side=Tkconstants.RIGHT) buttons.pack() def disablebuttons(self): self.sbotton.configure(state='disabled') self.pbotton.configure(state='disabled') self.qbotton.configure(state='disabled') def enablebuttons(self): self.sbotton.configure(state='normal') self.pbotton.configure(state='normal') self.qbotton.configure(state='normal') def show(self): self.deiconify() self.tkraise() def hide(self): self.withdraw() def get_outpath(self): cpath = self.outpath.get() if sys.platform.startswith("win"): # tk_chooseDirectory is horribly broken for unicode paths # on windows - bug has been reported but not fixed for years # workaround by using our own unicode aware version outpath = AskFolder(message="Choose the folder for DRM-free ebooks", defaultLocation=cpath) else: outpath = tkFileDialog.askdirectory( parent=None, title='Choose the folder for DRM-free ebooks', initialdir=cpath, initialfile=None) if outpath: outpath = os.path.normpath(outpath) self.outpath.delete(0, Tkconstants.END) self.outpath.insert(0, outpath) return def get_adkpath(self): cpath = self.adkpath.get() adkpath = tkFileDialog.askopenfilename(initialdir = cpath, parent=None, title='Select Adept Key file', defaultextension='.der', filetypes=[('Adept Key file', '.der'), ('All Files', '.*')]) if adkpath: adkpath = os.path.normpath(adkpath) self.adkpath.delete(0, Tkconstants.END) self.adkpath.insert(0, adkpath) return def get_kkpath(self): cpath = self.kkpath.get() kkpath = tkFileDialog.askopenfilename(initialdir = cpath, parent=None, title='Select Kindle Key file', defaultextension='.k4i', filetypes=[('Kindle Key file', '.k4i'), ('All Files', '.*')]) if kkpath: kkpath = os.path.normpath(kkpath) self.kkpath.delete(0, Tkconstants.END) self.kkpath.insert(0, kkpath) return def get_akkpath(self): cpath = self.akkpath.get() akkpath = tkFileDialog.askopenfilename(initialdir = cpath, parent=None, title='Select Android for Kindle backup file', defaultextension='.ab', filetypes=[('Kindle for Android backup file', '.ab'), ('All Files', '.*')]) if akkpath: akkpath = os.path.normpath(akkpath) self.akkpath.delete(0, Tkconstants.END) self.akkpath.insert(0, akkpath) return def get_bnkpath(self): cpath = self.bnkpath.get() bnkpath = tkFileDialog.askopenfilename(initialdir = cpath, parent=None, title='Select Barnes and Noble Key file', defaultextension='.b64', filetypes=[('Barnes and Noble Key file', '.b64'), ('All Files', '.*')]) if bnkpath: bnkpath = os.path.normpath(bnkpath) self.bnkpath.delete(0, Tkconstants.END) self.bnkpath.insert(0, bnkpath) return def get_bookpath(self): cpath = self.bookpath.get() bookpath = tkFileDialog.askopenfilename(parent=None, title='Select eBook for DRM Removal', filetypes=[('All Files', '.*'), ('ePub Files','.epub'), ('Kindle','.azw'), ('Kindle','.azw1'), ('Kindle','.azw3'), ('Kindle','.azw4'), ('Kindle','.tpz'), ('Kindle','.azw8'), ('Kindle','.kfx'), ('Kindle','.kfx-zip'), ('Kindle','.mobi'), ('Kindle','.prc'), ('eReader','.pdb'), ('PDF','.pdf')], initialdir=cpath) if bookpath: bookpath = os.path.normpath(bookpath) self.bookpath.delete(0, Tkconstants.END) self.bookpath.insert(0, bookpath) return def quitting(self): self.master.destroy() def setprefs(self): # setting new prefereces new_prefs = {} prefdir = self.prefs_array['dir'] new_prefs['dir'] = prefdir new_prefs['pids'] = self.pidinfo.get().replace(" ","") new_prefs['serials'] = self.serinfo.get().replace(" ","") new_prefs['sdrms'] = self.sdrminfo.get().strip().replace(", ",",") new_prefs['outdir'] = self.outpath.get().strip() adkpath = self.adkpath.get() if os.path.dirname(adkpath) != prefdir: new_prefs['adkfile'] = adkpath bnkpath = self.bnkpath.get() if os.path.dirname(bnkpath) != prefdir: new_prefs['bnkfile'] = bnkpath kkpath = self.kkpath.get() if os.path.dirname(kkpath) != prefdir: new_prefs['kindlefile'] = kkpath akkpath = self.akkpath.get() if os.path.dirname(akkpath) != prefdir: new_prefs['androidfile'] = akkpath self.master.setPreferences(new_prefs) # and update internal copies self.prefs_array['pids'] = new_prefs['pids'] self.prefs_array['serials'] = new_prefs['serials'] self.prefs_array['sdrms'] = new_prefs['sdrms'] self.prefs_array['outdir'] = new_prefs['outdir'] def doit(self): self.disablebuttons() filenames=[] bookpath = self.bookpath.get() bookpath = os.path.abspath(bookpath) filenames.append(bookpath) self.master.cd.doit(self.prefs_array,filenames) class ConvDialog(Toplevel): def __init__(self, master, prefs_array={}, filenames=[]): Toplevel.__init__(self, master) self.withdraw() self.protocol("WM_DELETE_WINDOW", self.withdraw) self.title("DeDRM Processing") self.master = master self.apphome = self.master.apphome self.prefs_array = prefs_array self.filenames = filenames self.interval = 50 self.p2 = None self.q = Queue() self.running = 'inactive' self.numgood = 0 self.numbad = 0 self.status = Tkinter.Label(self, text='DeDRM processing...') self.status.pack(fill=Tkconstants.X, expand=1) body = Tkinter.Frame(self) body.pack(fill=Tkconstants.X, expand=1) sticky = Tkconstants.E + Tkconstants.W body.grid_columnconfigure(1, weight=2) Tkinter.Label(body, text='Activity Bar').grid(row=0, sticky=Tkconstants.E) self.bar = ActivityBar(body, length=80, height=15, barwidth=5) self.bar.grid(row=0, column=1, sticky=sticky) msg1 = '' self.stext = ScrolledText(body, bd=5, relief=Tkconstants.RIDGE, height=4, width=80, wrap=Tkconstants.WORD) self.stext.grid(row=2, column=0, columnspan=2,sticky=sticky) self.stext.insert(Tkconstants.END,msg1) buttons = Tkinter.Frame(self) buttons.pack() self.qbutton = Tkinter.Button(buttons, text="Quit", width=14, command=self.quitting) self.qbutton.pack(side=Tkconstants.BOTTOM) self.status['text'] = '' self.logfile = open(os.path.join(os.path.expanduser('~'),'DeDRM.log'),'w') def show(self): self.deiconify() self.tkraise() def hide(self): self.withdraw() def doit(self, prefs, filenames): self.running = 'inactive' self.prefs_array = prefs self.filenames = filenames self.show() self.processBooks() def conversion_done(self): self.hide() self.master.alldone() def processBooks(self): while self.running == 'inactive': rscpath = self.prefs_array['dir'] filename = None if len(self.filenames) > 0: filename = self.filenames.pop(0) if filename == None: msg = 'Complete: ' msg += 'Successes: %d, ' % self.numgood msg += 'Failures: %d\n' % self.numbad self.showCmdOutput(msg) if self.numbad == 0: self.after(2000,self.conversion_done()) self.logfile.write("DeDRM v{0}: {1}".format(__version__,msg)) self.logfile.close() return infile = filename bname = os.path.basename(infile) msg = 'Processing: ' + bname + '...' self.logfile.write("DeDRM v{0}: {1}\n".format(__version__,msg)) self.showCmdOutput(msg) outdir = os.path.dirname(filename) if 'outdir' in self.prefs_array: dpath = self.prefs_array['outdir'] if dpath.strip() != '': outdir = dpath rv = self.decrypt_ebook(infile, outdir, rscpath) if rv == 0: self.bar.start() self.running = 'active' self.processQueue() else: msg = 'Unknown File: ' + bname + '\n' self.logfile.write("DeDRM v{0}: {1}".format(__version__,msg)) self.showCmdOutput(msg) self.numbad += 1 def quitting(self): # kill any still running subprocess self.running = 'stopped' if self.p2 != None: if (self.p2.exitcode == None): self.p2.terminate() self.conversion_done() # post output from subprocess in scrolled text widget def showCmdOutput(self, msg): if msg and msg !='': if sys.platform.startswith('win'): msg = msg.replace('\r\n','\n') self.stext.insert(Tkconstants.END,msg) self.stext.yview_pickplace(Tkconstants.END) return # read from subprocess pipe without blocking # invoked every interval via the widget "after" # option being used, so need to reset it for the next time def processQueue(self): if self.p2 == None: # nothing to wait for so just return return poll = self.p2.exitcode #print "processing", poll done = False text = '' while not done: try: data = self.q.get_nowait() text += data except Empty: done = True if text != '': self.logfile.write(text) if poll != None: self.bar.stop() if poll == 0: msg = 'Success\n' self.numgood += 1 else: msg = 'Failed\n' self.numbad += 1 self.p2.join() self.logfile.write("DeDRM v{0}: {1}\n".format(__version__,msg)) self.showCmdOutput(msg) self.p2 = None self.running = 'inactive' self.after(50,self.processBooks) return # make sure we get invoked again by event loop after interval self.stext.after(self.interval,self.processQueue) return def decrypt_ebook(self, infile, outdir, rscpath): q = self.q rv = 1 name, ext = os.path.splitext(os.path.basename(infile)) ext = ext.lower() if ext == '.epub': self.p2 = Process(target=processEPUB, args=(q, infile, outdir, rscpath)) self.p2.start() return 0 if ext == '.pdb': self.p2 = Process(target=processPDB, args=(q, infile, outdir, rscpath)) self.p2.start() return 0 if ext in ['.azw', '.azw1', '.azw3', '.azw4', '.prc', '.mobi', '.pobi', '.tpz', '.azw8', '.kfx', '.kfx-zip']: self.p2 = Process(target=processK4MOBI,args=(q, infile, outdir, rscpath)) self.p2.start() return 0 if ext == '.pdf': self.p2 = Process(target=processPDF, args=(q, infile, outdir, rscpath)) self.p2.start() return 0 return rv # child process starts here def processK4MOBI(q, infile, outdir, rscpath): add_cp65001_codec() set_utf8_default_encoding() sys.stdout = QueuedUTF8Stream(sys.stdout, q) sys.stderr = QueuedUTF8Stream(sys.stderr, q) rv = decryptk4mobi(infile, outdir, rscpath) sys.exit(rv) # child process starts here def processPDF(q, infile, outdir, rscpath): add_cp65001_codec() set_utf8_default_encoding() sys.stdout = QueuedUTF8Stream(sys.stdout, q) sys.stderr = QueuedUTF8Stream(sys.stderr, q) rv = decryptpdf(infile, outdir, rscpath) sys.exit(rv) # child process starts here def processEPUB(q, infile, outdir, rscpath): add_cp65001_codec() set_utf8_default_encoding() sys.stdout = QueuedUTF8Stream(sys.stdout, q) sys.stderr = QueuedUTF8Stream(sys.stderr, q) rv = decryptepub(infile, outdir, rscpath) sys.exit(rv) # child process starts here def processPDB(q, infile, outdir, rscpath): add_cp65001_codec() set_utf8_default_encoding() sys.stdout = QueuedUTF8Stream(sys.stdout, q) sys.stderr = QueuedUTF8Stream(sys.stderr, q) rv = decryptpdb(infile, outdir, rscpath) sys.exit(rv) def main(): argv=unicode_argv() apphome = os.path.dirname(argv[0]) apphome = os.path.abspath(apphome) # windows may pass a spurious quoted null string as argv[1] from bat file # simply work around this until we can figure out a better way to handle things if sys.platform.startswith('win') and len(argv) == 2: temp = argv[1] temp = temp.strip('"') temp = temp.strip() if temp == '': argv.pop() if len(argv) == 1: filenames = [] dnd = False else : # processing books via drag and drop dnd = True # build a list of the files to be processed # note all filenames and paths have been utf-8 encoded infilelst = argv[1:] filenames = [] for infile in infilelst: infile = infile.replace('"','') infile = os.path.abspath(infile) if os.path.isdir(infile): bpath = infile filelst = os.listdir(infile) for afile in filelst: if not afile.startswith('.'): filepath = os.path.join(bpath,afile) if os.path.isfile(filepath): filenames.append(filepath) else : afile = os.path.basename(infile) if not afile.startswith('.'): if os.path.isfile(infile): filenames.append(infile) # start up gui app app = MainApp(apphome, dnd, filenames) app.mainloop() return 0 if __name__ == "__main__": sys.exit(main())