diff --git a/Calibre_Plugins/ineptepub_plugin.zip b/Calibre_Plugins/ineptepub_plugin.zip
index 31ffcde..562923b 100644
Binary files a/Calibre_Plugins/ineptepub_plugin.zip and b/Calibre_Plugins/ineptepub_plugin.zip differ
diff --git a/Calibre_Plugins/ineptepub_plugin/ade_key.py b/Calibre_Plugins/ineptepub_plugin/ade_key.py
index 9c7f6cf..eb9ae3d 100644
--- a/Calibre_Plugins/ineptepub_plugin/ade_key.py
+++ b/Calibre_Plugins/ineptepub_plugin/ade_key.py
@@ -310,35 +310,31 @@ if iswindows:
else:
import xml.etree.ElementTree as etree
- import Carbon.File
- import Carbon.Folder
- import Carbon.Folders
- import MacOS
-
- ACTIVATION_PATH = 'Adobe/Digital Editions/activation.dat'
+ import subprocess
+
NSMAP = {'adept': 'http://ns.adobe.com/adept',
'enc': 'http://www.w3.org/2001/04/xmlenc#'}
-
- def find_folder(domain, dtype):
- try:
- fsref = Carbon.Folder.FSFindFolder(domain, dtype, False)
- return Carbon.File.pathname(fsref)
- except MacOS.Error:
- return None
-
- def find_app_support_file(subpath):
- dtype = Carbon.Folders.kApplicationSupportFolderType
- for domain in Carbon.Folders.kUserDomain, Carbon.Folders.kLocalDomain:
- path = find_folder(domain, dtype)
- if path is None:
- continue
- path = os.path.join(path, subpath)
- if os.path.isfile(path):
- return path
+
+ def findActivationDat():
+ home = os.getenv('HOME')
+ cmdline = 'find "' + home + '/Library/Application Support/Adobe/Digital Editions" -name "activation.dat"'
+ cmdline = cmdline.encode(sys.getfilesystemencoding())
+ p2 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
+ out1, out2 = p2.communicate()
+ reslst = out1.split('\n')
+ cnt = len(reslst)
+ for j in xrange(cnt):
+ resline = reslst[j]
+ pp = resline.find('activation.dat')
+ if pp >= 0:
+ ActDatPath = resline
+ break
+ if os.path.exists(ActDatPath):
+ return ActDatPath
return None
def retrieve_key():
- actpath = find_app_support_file(ACTIVATION_PATH)
+ actpath = findActivationDat()
if actpath is None:
raise ADEPTError("Could not locate ADE activation")
tree = etree.parse(actpath)
diff --git a/Calibre_Plugins/ineptepub_plugin/ineptepub_plugin.py b/Calibre_Plugins/ineptepub_plugin/ineptepub_plugin.py
index ea52f0a..137cc2a 100644
--- a/Calibre_Plugins/ineptepub_plugin/ineptepub_plugin.py
+++ b/Calibre_Plugins/ineptepub_plugin/ineptepub_plugin.py
@@ -42,7 +42,9 @@
# Revision history:
# 0.1 - Initial release
# 0.1.1 - Allow Windows users to make use of openssl if they have it installed.
-# - Incorporated SomeUpdates zipfix routine.
+# - Incorporated SomeUpdates zipfix routine.
+# 0.1.2 - Removed Carbon dependency for Mac users. Fixes an issue that was a
+# result of Calibre changing to python 2.7.
"""
@@ -363,7 +365,7 @@ class IneptDeDRM(FileTypePlugin):
Credit given to I <3 Cabbages for the original stand-alone scripts.'
supported_platforms = ['linux', 'osx', 'windows']
author = 'DiapDealer'
- version = (0, 1, 1)
+ version = (0, 1, 2)
minimum_calibre_version = (0, 6, 44) # Compiled python libraries cannot be imported in earlier versions.
file_types = set(['epub'])
on_import = True
@@ -420,7 +422,7 @@ class IneptDeDRM(FileTypePlugin):
try:
keydata = retrieve_key()
userkeys.append(keydata)
- keypath = os.path.join(confpath, 'adeptkey.der')
+ keypath = os.path.join(confpath, 'calibre-adeptkey.der')
with open(keypath, 'wb') as f:
f.write(keydata)
print 'IneptEpub: Created keyfile from ADE install.'
diff --git a/Calibre_Plugins/ineptepub_plugin/osx/Carbon/File.pyo b/Calibre_Plugins/ineptepub_plugin/osx/Carbon/File.pyo
deleted file mode 100644
index be3ab04..0000000
Binary files a/Calibre_Plugins/ineptepub_plugin/osx/Carbon/File.pyo and /dev/null differ
diff --git a/Calibre_Plugins/ineptepub_plugin/osx/Carbon/Folder.pyo b/Calibre_Plugins/ineptepub_plugin/osx/Carbon/Folder.pyo
deleted file mode 100644
index 5a82134..0000000
Binary files a/Calibre_Plugins/ineptepub_plugin/osx/Carbon/Folder.pyo and /dev/null differ
diff --git a/Calibre_Plugins/ineptepub_plugin/osx/Carbon/Folders.pyo b/Calibre_Plugins/ineptepub_plugin/osx/Carbon/Folders.pyo
deleted file mode 100644
index c5a2ee0..0000000
Binary files a/Calibre_Plugins/ineptepub_plugin/osx/Carbon/Folders.pyo and /dev/null differ
diff --git a/Calibre_Plugins/ineptepub_plugin/osx/Carbon/__init__.pyo b/Calibre_Plugins/ineptepub_plugin/osx/Carbon/__init__.pyo
deleted file mode 100644
index 0f5325d..0000000
Binary files a/Calibre_Plugins/ineptepub_plugin/osx/Carbon/__init__.pyo and /dev/null differ
diff --git a/Calibre_Plugins/k4mobidedrm_plugin.zip b/Calibre_Plugins/k4mobidedrm_plugin.zip
index 21a61cb..fe8263a 100644
Binary files a/Calibre_Plugins/k4mobidedrm_plugin.zip and b/Calibre_Plugins/k4mobidedrm_plugin.zip differ
diff --git a/Calibre_Plugins/k4mobidedrm_plugin/k4mobidedrm_plugin.py b/Calibre_Plugins/k4mobidedrm_plugin/k4mobidedrm_plugin.py
index 1ad83bc..2b91046 100644
--- a/Calibre_Plugins/k4mobidedrm_plugin/k4mobidedrm_plugin.py
+++ b/Calibre_Plugins/k4mobidedrm_plugin/k4mobidedrm_plugin.py
@@ -475,7 +475,7 @@ if not __name__ == "__main__" and inCalibre:
Provided by the work of many including DiapDealer, SomeUpdates, IHeartCabbages, CMBDTC, Skindle, DarkReverser, ApprenticeAlf, etc.'
supported_platforms = ['osx', 'windows', 'linux'] # Platforms this plugin will run on
author = 'DiapDealer, SomeUpdates' # The author of this plugin
- version = (0, 1, 3) # The version number of this plugin
+ version = (0, 1, 4) # The version number of this plugin
file_types = set(['prc','mobi','azw']) # The file types that this plugin will be applied to
on_import = True # Run this plugin during the import
priority = 200 # run this plugin before mobidedrm, k4pcdedrm, k4dedrm
@@ -483,6 +483,24 @@ if not __name__ == "__main__" and inCalibre:
def run(self, path_to_ebook):
from calibre.gui2 import is_ok_to_use_qt
from PyQt4.Qt import QMessageBox
+
+ # Head Topaz files off at the pass and warn the user that they will NOT
+ # be decrypted. Changes the file extension from .azw or .prc to .tpz so
+ # Calibre can at least read the metadata properly and the user can find
+ # them by sorting on 'format'.
+ with open(path_to_ebook, 'rb') as f:
+ raw = f.read()
+ if raw.startswith('TPZ'):
+ tf = self.temporary_file('.tpz')
+ if is_ok_to_use_qt():
+ d = QMessageBox(QMessageBox.Warning, "K4MobiDeDRM Plugin", "%s is a Topaz book. It will NOT be decrypted!" % path_to_ebook)
+ d.show()
+ d.raise_()
+ d.exec_()
+ tf.write(raw)
+ tf.close
+ return tf.name
+
global kindleDatabase
global openKindleInfo, CryptUnprotectData, GetUserName, GetVolumeSerialNumber, charMap1, charMap2, charMap3, charMap4
if sys.platform.startswith('win'):
diff --git a/Calibre_Plugins/k4mobidedrm_plugin/mobidedrm.py b/Calibre_Plugins/k4mobidedrm_plugin/mobidedrm.py
index 9f17a3b..183432c 100644
--- a/Calibre_Plugins/k4mobidedrm_plugin/mobidedrm.py
+++ b/Calibre_Plugins/k4mobidedrm_plugin/mobidedrm.py
@@ -39,8 +39,9 @@
# Removed the disabled Calibre plug-in code
# Permit use of 8-digit PIDs
# 0.19 - It seems that multibyte entries aren't encrypted in a v6 file either.
+# 0.20 - Corretion: It seems that multibyte entries are encrypted in a v6 file.
-__version__ = '0.19'
+__version__ = '0.20'
import sys
import struct
@@ -208,8 +209,8 @@ class DrmStripper:
if (mobi_length >= 0xE4) and (mobi_version >= 5):
extra_data_flags, = struct.unpack('>H', sect[0xF2:0xF4])
print "Extra Data Flags = %d" %extra_data_flags
- if mobi_version <= 5:
- # multibyte utf8 data is included in the encryption for mobi_version 5 and below
+ if mobi_version < 7:
+ # multibyte utf8 data is included in the encryption for mobi_version 6 and below
# so clear that byte so that we leave it to be decrypted.
extra_data_flags &= 0xFFFE
diff --git a/DeDRM_Macintosh_Application/DeDRM.app.txt b/DeDRM_Macintosh_Application/DeDRM.app.txt
index 099e256..63a9790 100644
--- a/DeDRM_Macintosh_Application/DeDRM.app.txt
+++ b/DeDRM_Macintosh_Application/DeDRM.app.txt
@@ -162,7 +162,7 @@ on unlockpdbfile(encryptedFile)
set sourcePath to (POSIX path of file parent_folder) & fileName & "_Source/"
set pmlzFilePath to POSIX path of file parent_folder & fileName & "_dedrmed.pmlz"
if length of bnKeys is 0 then
- GetKeys()
+ GetKeys(false)
end if
set shellresult to "Error: No eReader/Barnes & Noble Name:Number keys supplied. Can't decrypt."
repeat with BNKey in bnKeys
@@ -367,7 +367,7 @@ end unlockepubfile
on unlockpdffile(encryptedFile)
--check it's an ePub file.
- set PDFSig to "NOT_DF"
+ set PDFSig to "NOT_PDF"
try
set PDFSig to read file encryptedFile from 1 for 4
end try
@@ -393,23 +393,7 @@ on unlockpdffile(encryptedFile)
set decoded to "NO"
-- first we must check we have a PDF script
- if not fileexists(AdobePDFTool) then
- set newFile to AdobePDFTool
- try
- tell me to activate
- set newFile to ((choose file with prompt "Please find the ineptpdf script") as text)
- end try
- if not fileexists(newFile) then
- set ErrorCount to ErrorCount + 1
- set ErrorList to ErrorList & encryptedFile & " is a PDF file and no ineptpdf script found.
-
-"
- return
- end if
- -- set AdobePDFTool to POSIX path of file CopyToPrefs(newFile)
- set AdobePDFTool to POSIX path of file newFile
- WritePrefs()
- end if
+ GetIneptPDF(false)
if not fileexists(AdobePDFTool) then
set ErrorCount to ErrorCount + 1
set ErrorList to ErrorList & encryptedFile & " is a PDF file and no ineptpdf script found.
@@ -596,7 +580,7 @@ on GetPIDs()
Enter any additional Mobipocket PIDs for your Mobipocket books one at a time:"
set FinishedButton to "No More"
end if
- set dialogresult to (display dialog DialogPrompt default answer "" buttons {"Delete All", "Add", FinishedButton} with title "DeDRM Applescript" default button 2)
+ set dialogresult to (display dialog DialogPrompt default answer "" buttons {"Delete All", "Add", FinishedButton} with title "DeDRM Applescript 2/5" default button 2)
if button returned of dialogresult is "Add" then
set PID to text returned of dialogresult
set PIDlength to length of PID
@@ -657,7 +641,37 @@ To add extra Kindle Info files, click the Add
end repeat
end GetKindleInfoFiles
-on GetKeys()
+on GetIneptPDF(always)
+ if always or not fileexists(AdobePDFTool) then
+ set newFile to ""
+ try
+ tell me to activate
+ if (always) then
+ set promptstring to "DeDRM Applescript 5/5
+"
+ else
+ set promptstring to "DeDRM Applescript
+"
+ end if
+ if fileexists(AdobePDFTool) then
+ set promptstring to promptstring & "Please find the new ineptpdf script or click Cancel if the path shown is correct:
+" & ((POSIX file AdobePDFTool) as text)
+ else
+ set promptstring to promptstring & "Please find the ineptpdf script to be able to dedrm PDF files."
+ end if
+ set newFile to ((choose file with prompt promptstring default location POSIX file AdobePDFTool) as text)
+ --on error errormessage
+ -- display dialog errormessage
+ end try
+ if fileexists(newFile) then
+ -- set AdobePDFTool to POSIX path of file CopyToPrefs(newFile)
+ set AdobePDFTool to POSIX path of file newFile
+ WritePrefs()
+ end if
+ end if
+end GetIneptPDF
+
+on GetKeys(running)
set bnKeyText to ""
repeat
set BNKeystring to GetBNKeystring()
@@ -671,9 +685,12 @@ on GetKeys()
Please enter any additional "
set FinishedButton to "No More"
end if
- set DialogPrompt to DialogPrompt & "eReader/Barnes & Noble Name:Number key pairs one at a time. If you're only decoding eReader files, the last 8 digits of the Number will do. The full 16 are only needed for Barnes & Noble ePubs. Only the last eight will be stored or displayed. Please separate the name and number with a colon and click \"Add\". Or to add a an already generated .b64 file, just click \"Add\" with nothing in the text field."
-
- set dialogresult to (display dialog DialogPrompt default answer bnKeyText buttons {"Delete All", "Add", FinishedButton} with title "DeDRM Applescript" default button 2)
+ set DialogPrompt to DialogPrompt & "eReader/Barnes & Noble Name,Number key pairs one at a time. If you're only decoding eReader files, the last 8 digits of the Number will do. The full 15 or 16 are only needed for Barnes & Noble ePubs. Only the last eight will be stored or displayed. Please separate the name and number with a comma and click \"Add\". Or to add a an already generated .b64 file, just click \"Add\" with nothing in the text field."
+ set dialogtitle to "DeDRM Applescript"
+ if (running) then
+ set dialogtitle to dialogtitle & " 3/5"
+ end if
+ set dialogresult to (display dialog DialogPrompt default answer bnKeyText buttons {"Delete All", "Add", FinishedButton} with title dialogtitle default button 2)
if button returned of dialogresult is "Add" then
set bnKeyText to text returned of dialogresult
if bnKeyText is "" then
@@ -686,17 +703,17 @@ Please enter any additional "
display dialog message
end if
end try
- else if not (bnKeyText contains ":") then
- display dialog "Name and Number must be separated by a colon (:)." buttons {"OK"} default button 1 with title "DeDRM Applescript" with icon caution
+ else if not (bnKeyText contains ",") then
+ display dialog "Name and Number must be separated by a comma (,)." buttons {"OK"} default button 1 with title "DeDRM Applescript" with icon caution
else
- set AppleScript's text item delimiters to ":"
+ set AppleScript's text item delimiters to ","
set keyNumber to the last text item of bnKeyText
set keyName to (text items 1 through -2 of bnKeyText) as string
- if ((length of keyNumber) = 16 or (length of keyNumber) = 8) and (length of keyName) > 0 then
+ if ((length of keyNumber) = 16 or (length of keyNumber) = 15 or (length of keyNumber) = 8) and (length of keyName) > 0 then
set shellresult to ""
set keyfilepath to ""
- if (length of keyNumber) = 16 then
+ if (length of keyNumber) = 16 or (length of keyNumber) = 15 then
-- get the B&N key from this pair
set shellresult to "no result"
set scriptError to "Key Gen Script failed."
@@ -717,7 +734,7 @@ Please enter any additional "
display dialog "Error generating key from this info, error message was: " & scriptError buttons {"OK"} default button 1 with title "DeDRM Applescript" with icon caution
end if
else
- display dialog "Key numbers must be 8 or 16 characters long (no spaces) and the key name must not be absent." buttons {"OK"} default button 1 with title "DeDRM Applescript" with icon caution
+ display dialog "Key numbers must be 8 or 15 or 16 characters long (no spaces) and the key name must not be absent." buttons {"OK"} default button 1 with title "DeDRM Applescript" with icon caution
end if
end if
else if button returned of dialogresult is "Delete All" then
@@ -759,7 +776,7 @@ on GetAdeptKeyFiles()
To add extra key files (.der), click the AddÉ button."
set FinishedButton to "No More"
end if
- set dialogresult to (display dialog DialogPrompt buttons {"Forget All", "AddÉ", FinishedButton} with title "DeDRM Applescript" default button 2)
+ set dialogresult to (display dialog DialogPrompt buttons {"Forget All", "AddÉ", FinishedButton} with title "DeDRM Applescript 4/5" default button 2)
if button returned of dialogresult is "AddÉ" then
try
set newFile to (choose file with prompt "Please select an Adept key file") as text
@@ -1044,12 +1061,13 @@ Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
For more information, please refer to
-" with title "DeDRM Applescript" buttons {"Cancel", "Continue"} default button 2
+" with title "DeDRM Applescript 1/5" buttons {"Cancel", "Continue"} default button 2
ReadPrefs()
GetPIDs()
- GetKeys()
+ GetKeys(true)
GetAdeptKey(true)
GetAdeptKeyFiles()
+ GetIneptPDF(true)
--GetKindleInfoFiles()
WritePrefs()
end if
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Info.plist b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Info.plist
index 330ec11..31358b6 100644
--- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Info.plist
+++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Info.plist
@@ -24,7 +24,7 @@
CFBundleExecutable
droplet
CFBundleGetInfoString
- DeDRM 1.2, Copyright © 2010 by Apprentice Alf.
+ DeDRM 1.3, Copyright © 2010 by Apprentice Alf.
CFBundleIconFile
droplet
CFBundleInfoDictionaryVersion
@@ -34,7 +34,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.2
+ 1.3
CFBundleSignature
dplt
LSMinimumSystemVersion
@@ -46,9 +46,9 @@
name
ScriptWindowState
positionOfDivider
- 885
+ 739
savedFrame
- 1507 -64 1262 964 1440 -150 1680 1050
+ 1533 -24 1262 818 1440 -150 1680 1050
selectedTabView
result
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/Scripts/main.scpt b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/Scripts/main.scpt
index 2a37d51..86a1173 100644
Binary files a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/Scripts/main.scpt and b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/Scripts/main.scpt differ
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/mobidedrm.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/mobidedrm.py
index 9f17a3b..183432c 100644
--- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/mobidedrm.py
+++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/mobidedrm.py
@@ -39,8 +39,9 @@
# Removed the disabled Calibre plug-in code
# Permit use of 8-digit PIDs
# 0.19 - It seems that multibyte entries aren't encrypted in a v6 file either.
+# 0.20 - Corretion: It seems that multibyte entries are encrypted in a v6 file.
-__version__ = '0.19'
+__version__ = '0.20'
import sys
import struct
@@ -208,8 +209,8 @@ class DrmStripper:
if (mobi_length >= 0xE4) and (mobi_version >= 5):
extra_data_flags, = struct.unpack('>H', sect[0xF2:0xF4])
print "Extra Data Flags = %d" %extra_data_flags
- if mobi_version <= 5:
- # multibyte utf8 data is included in the encryption for mobi_version 5 and below
+ if mobi_version < 7:
+ # multibyte utf8 data is included in the encryption for mobi_version 6 and below
# so clear that byte so that we leave it to be decrypted.
extra_data_flags &= 0xFFFE
diff --git a/Kindle_Mobi_Tools/FindTopazEbooks.pyw b/Kindle_Mobi_Tools/FindTopazEbooks.pyw
new file mode 100644
index 0000000..6a0df30
--- /dev/null
+++ b/Kindle_Mobi_Tools/FindTopazEbooks.pyw
@@ -0,0 +1,216 @@
+#!/usr/bin/env python
+
+# This is a simple tool to identify all Amazon Topaz ebooks in a specific directory.
+# There always seems to be confusion since Topaz books downloaded to K4PC/Mac can have
+# almost any extension (.azw, .azw1, .prc, tpz). While the .azw1 and .tpz extensions
+# are fairly easy to indentify, the others are not (without opening the files in an editor).
+
+# To run the tool with the GUI frontend, just double-click on the 'FindTopazFiles.pyw' file
+# and select the folder where all of the ebooks in question are located. Then click 'Search'.
+# The program will list the file names of the ebooks that are indentified as being Topaz.
+# You can then isolate those books and use the Topaz tools to decrypt and convert them.
+
+# You can also run the script from a command line... supplying the folder to search
+# as a parameter: python FindTopazEbooks.pyw "C:\My Folder" (change appropriately for
+# your particular O.S.)
+
+# ** NOTE: This program does NOT decrypt or modify Topaz files in any way. It simply identifies them.
+
+# PLEASE DO NOT PIRATE EBOOKS!
+
+# We want all authors and publishers, and eBook stores to live
+# long and prosperous lives but at the same time we just want to
+# be able to read OUR books on whatever device we want and to keep
+# readable for a long, long time
+
+# This borrows very heavily from works by CMBDTC, IHeartCabbages, skindle,
+# unswindle, DarkReverser, ApprenticeAlf, DiapDealer, some_updates
+# and many many others
+
+# Revision history:
+# 1 - Initial release.
+
+from __future__ import with_statement
+
+__license__ = 'GPL v3'
+
+import sys
+import os
+import re
+import shutil
+import Tkinter
+import Tkconstants
+import tkFileDialog
+import tkMessageBox
+
+
+class ScrolledText(Tkinter.Text):
+ def __init__(self, master=None, **kw):
+ self.frame = Tkinter.Frame(master)
+ self.vbar = Tkinter.Scrollbar(self.frame)
+ self.vbar.pack(side=Tkconstants.RIGHT, fill=Tkconstants.Y)
+ kw.update({'yscrollcommand': self.vbar.set})
+ Tkinter.Text.__init__(self, self.frame, **kw)
+ self.pack(side=Tkconstants.LEFT, fill=Tkconstants.BOTH, expand=True)
+ self.vbar['command'] = self.yview
+ # Copy geometry methods of self.frame without overriding Text
+ # methods = hack!
+ text_meths = vars(Tkinter.Text).keys()
+ methods = vars(Tkinter.Pack).keys() + vars(Tkinter.Grid).keys() + vars(Tkinter.Place).keys()
+ methods = set(methods).difference(text_meths)
+ for m in methods:
+ if m[0] != '_' and m != 'config' and m != 'configure':
+ setattr(self, m, getattr(self.frame, m))
+
+ def __str__(self):
+ return str(self.frame)
+
+
+def cli_main(argv=sys.argv, obj=None):
+ progname = os.path.basename(argv[0])
+ if len(argv) != 2:
+ print "usage: %s DIRECTORY" % (progname,)
+ return 1
+
+ if obj == None:
+ print "\nTopaz search results:\n"
+ else:
+ obj.stext.insert(Tkconstants.END,"Topaz search results:\n\n")
+
+ inpath = argv[1]
+ files = os.listdir(inpath)
+ filefilter = re.compile("(\.azw$)|(\.azw1$)|(\.prc$)|(\.tpz$)", re.IGNORECASE)
+ files = filter(filefilter.search, files)
+
+ if files:
+ topazcount = 0
+ totalcount = 0
+ for filename in files:
+ with open(os.path.join(inpath, filename), 'rb') as f:
+ try:
+ if f.read().startswith('TPZ'):
+ f.close()
+ basename, extension = os.path.splitext(filename)
+ if obj == None:
+ print " %s is a Topaz formatted ebook." % filename
+ """
+ if extension == '.azw' or extension == '.prc':
+ print " renaming to %s" % (basename + '.tpz')
+ shutil.move(os.path.join(inpath, filename),
+ os.path.join(inpath, basename + '.tpz'))
+ """
+ else:
+ msg1 = " %s is a Topaz formatted ebook.\n" % filename
+ obj.stext.insert(Tkconstants.END,msg1)
+ """
+ if extension == '.azw' or extension == '.prc':
+ msg2 = " renaming to %s\n" % (basename + '.tpz')
+ obj.stext.insert(Tkconstants.END,msg2)
+ shutil.move(os.path.join(inpath, filename),
+ os.path.join(inpath, basename + '.tpz'))
+ """
+ topazcount += 1
+ except:
+ if obj == None:
+ print " Error reading %s." % filename
+ else:
+ msg = " Error reading or %s.\n" % filename
+ obj.stext.insert(Tkconstants.END,msg)
+ pass
+ totalcount += 1
+ if topazcount == 0:
+ if obj == None:
+ print "\nNo Topaz books found in %s." % inpath
+ else:
+ msg = "\nNo Topaz books found in %s.\n\n" % inpath
+ obj.stext.insert(Tkconstants.END,msg)
+ else:
+ if obj == None:
+ print "\n%i Topaz books found in %s\n%i total books checked.\n" % (topazcount, inpath, totalcount)
+ else:
+ msg = "\n%i Topaz books found in %s\n%i total books checked.\n\n" %(topazcount, inpath, totalcount)
+ obj.stext.insert(Tkconstants.END,msg)
+ else:
+ if obj == None:
+ print "No typical Topaz file extensions found in %s.\n" % inpath
+ else:
+ msg = "No typical Topaz file extensions found in %s.\n\n" % inpath
+ obj.stext.insert(Tkconstants.END,msg)
+
+ return 0
+
+
+class DecryptionDialog(Tkinter.Frame):
+ def __init__(self, root):
+ Tkinter.Frame.__init__(self, root, border=5)
+ ltext='Search a directory for Topaz eBooks\n'
+ self.status = Tkinter.Label(self, text=ltext)
+ 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='Directory to Search').grid(row=1)
+ self.inpath = Tkinter.Entry(body, width=30)
+ self.inpath.grid(row=1, column=1, sticky=sticky)
+ button = Tkinter.Button(body, text="...", command=self.get_inpath)
+ button.grid(row=1, column=2)
+ msg1 = 'Topaz search results \n\n'
+ self.stext = ScrolledText(body, bd=5, relief=Tkconstants.RIDGE,
+ height=15, width=60, wrap=Tkconstants.WORD)
+ self.stext.grid(row=4, column=0, columnspan=2,sticky=sticky)
+ #self.stext.insert(Tkconstants.END,msg1)
+ buttons = Tkinter.Frame(self)
+ buttons.pack()
+
+
+ self.botton = Tkinter.Button(
+ buttons, text="Search", width=10, command=self.search)
+ self.botton.pack(side=Tkconstants.LEFT)
+ Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
+ self.button = Tkinter.Button(
+ buttons, text="Quit", width=10, command=self.quit)
+ self.button.pack(side=Tkconstants.RIGHT)
+
+ def get_inpath(self):
+ cwd = os.getcwdu()
+ cwd = cwd.encode('utf-8')
+ inpath = tkFileDialog.askdirectory(
+ parent=None, title='Directory to search',
+ initialdir=cwd, initialfile=None)
+ if inpath:
+ inpath = os.path.normpath(inpath)
+ self.inpath.delete(0, Tkconstants.END)
+ self.inpath.insert(0, inpath)
+ return
+
+
+ def search(self):
+ inpath = self.inpath.get()
+ if not inpath or not os.path.exists(inpath):
+ self.status['text'] = 'Specified directory does not exist'
+ return
+ argv = [sys.argv[0], inpath]
+ self.status['text'] = 'Searching...'
+ self.botton.configure(state='disabled')
+ cli_main(argv, self)
+ self.status['text'] = 'Search a directory for Topaz files'
+ self.botton.configure(state='normal')
+
+ return
+
+
+def gui_main():
+ root = Tkinter.Tk()
+ root.title('Topaz eBook Finder')
+ root.resizable(True, False)
+ root.minsize(370, 0)
+ DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1)
+ root.mainloop()
+ return 0
+
+
+if __name__ == '__main__':
+ if len(sys.argv) > 1:
+ sys.exit(cli_main())
+ sys.exit(gui_main())
\ No newline at end of file
diff --git a/Kindle_Mobi_Tools/K4_Mobi_DeDRM_Combined_Tool/K4MobiDeDRM.pyw b/Kindle_Mobi_Tools/K4_Mobi_DeDRM_Combined_Tool/K4MobiDeDRM.pyw
index d25c029..c909eb3 100644
--- a/Kindle_Mobi_Tools/K4_Mobi_DeDRM_Combined_Tool/K4MobiDeDRM.pyw
+++ b/Kindle_Mobi_Tools/K4_Mobi_DeDRM_Combined_Tool/K4MobiDeDRM.pyw
@@ -192,6 +192,19 @@ class MainDialog(Tkinter.Frame):
self.status['text'] = 'Specified K4PC, K4M or Mobi eBook file does not exist'
self.sbotton.configure(state='normal')
return
+ # Head all Topaz ebooks off at the pass and warn user.
+ with open(mobipath, 'rb') as f:
+ raw = f.read()
+ if raw.startswith('TPZ'):
+ f.close()
+ tkMessageBox.showerror(
+ "K4MobiDeDRM",
+ "%s is a Topaz ebook. It cannot be decrypted with this tool. "
+ "You must use the Topaz Tools for this particular ebook." % mobipath)
+ self.status['text'] = 'The selected file is a Topaz ebook! Use Topaz tools.'
+ self.sbotton.configure(state='normal')
+ return
+ f.close()
if not outpath:
self.status['text'] = 'No output directory specified'
self.sbotton.configure(state='normal')
diff --git a/Kindle_Mobi_Tools/K4_Mobi_DeDRM_Combined_Tool/lib/k4mobidedrm.py b/Kindle_Mobi_Tools/K4_Mobi_DeDRM_Combined_Tool/lib/k4mobidedrm.py
index df05f89..ee6ebd9 100644
--- a/Kindle_Mobi_Tools/K4_Mobi_DeDRM_Combined_Tool/lib/k4mobidedrm.py
+++ b/Kindle_Mobi_Tools/K4_Mobi_DeDRM_Combined_Tool/lib/k4mobidedrm.py
@@ -475,7 +475,7 @@ if not __name__ == "__main__" and inCalibre:
Provided by the work of many including DiapDealer, SomeUpdates, IHeartCabbages, CMBDTC, Skindle, DarkReverser, ApprenticeAlf, etc.'
supported_platforms = ['osx', 'windows', 'linux'] # Platforms this plugin will run on
author = 'DiapDealer, SomeUpdates' # The author of this plugin
- version = (0, 1, 2) # The version number of this plugin
+ version = (0, 1, 3) # The version number of this plugin
file_types = set(['prc','mobi','azw']) # The file types that this plugin will be applied to
on_import = True # Run this plugin during the import
priority = 200 # run this plugin before mobidedrm, k4pcdedrm, k4dedrm
@@ -483,6 +483,24 @@ if not __name__ == "__main__" and inCalibre:
def run(self, path_to_ebook):
from calibre.gui2 import is_ok_to_use_qt
from PyQt4.Qt import QMessageBox
+
+ # Head Topaz files off at the pass and warn the user that they will NOT
+ # be decrypted. Changes the file extension from .azw or .prc to .tpz so
+ # Calibre can at least read the metadata properly and the user can find
+ # them by sorting on 'format'.
+ with open(path_to_ebook, 'rb') as f:
+ raw = f.read()
+ if raw.startswith('TPZ'):
+ tf = self.temporary_file('.tpz')
+ if is_ok_to_use_qt():
+ d = QMessageBox(QMessageBox.Warning, "K4MobiDeDRM Plugin", "%s is a Topaz book. It will NOT be decrypted!" % path_to_ebook)
+ d.show()
+ d.raise_()
+ d.exec_()
+ tf.write(raw)
+ tf.close
+ return tf.name
+
global kindleDatabase
global openKindleInfo, CryptUnprotectData, GetUserName, GetVolumeSerialNumber, charMap1, charMap2, charMap3, charMap4
if sys.platform.startswith('win'):
diff --git a/Kindle_Mobi_Tools/K4_Mobi_DeDRM_Combined_Tool/lib/mobidedrm.py b/Kindle_Mobi_Tools/K4_Mobi_DeDRM_Combined_Tool/lib/mobidedrm.py
index 9f17a3b..183432c 100644
--- a/Kindle_Mobi_Tools/K4_Mobi_DeDRM_Combined_Tool/lib/mobidedrm.py
+++ b/Kindle_Mobi_Tools/K4_Mobi_DeDRM_Combined_Tool/lib/mobidedrm.py
@@ -39,8 +39,9 @@
# Removed the disabled Calibre plug-in code
# Permit use of 8-digit PIDs
# 0.19 - It seems that multibyte entries aren't encrypted in a v6 file either.
+# 0.20 - Corretion: It seems that multibyte entries are encrypted in a v6 file.
-__version__ = '0.19'
+__version__ = '0.20'
import sys
import struct
@@ -208,8 +209,8 @@ class DrmStripper:
if (mobi_length >= 0xE4) and (mobi_version >= 5):
extra_data_flags, = struct.unpack('>H', sect[0xF2:0xF4])
print "Extra Data Flags = %d" %extra_data_flags
- if mobi_version <= 5:
- # multibyte utf8 data is included in the encryption for mobi_version 5 and below
+ if mobi_version < 7:
+ # multibyte utf8 data is included in the encryption for mobi_version 6 and below
# so clear that byte so that we leave it to be decrypted.
extra_data_flags &= 0xFFFE
diff --git a/Kindle_Mobi_Tools/Kindle_4_Mac_Unswindle/lib/mobidedrm.py b/Kindle_Mobi_Tools/Kindle_4_Mac_Unswindle/lib/mobidedrm.py
index 9f17a3b..183432c 100644
--- a/Kindle_Mobi_Tools/Kindle_4_Mac_Unswindle/lib/mobidedrm.py
+++ b/Kindle_Mobi_Tools/Kindle_4_Mac_Unswindle/lib/mobidedrm.py
@@ -39,8 +39,9 @@
# Removed the disabled Calibre plug-in code
# Permit use of 8-digit PIDs
# 0.19 - It seems that multibyte entries aren't encrypted in a v6 file either.
+# 0.20 - Corretion: It seems that multibyte entries are encrypted in a v6 file.
-__version__ = '0.19'
+__version__ = '0.20'
import sys
import struct
@@ -208,8 +209,8 @@ class DrmStripper:
if (mobi_length >= 0xE4) and (mobi_version >= 5):
extra_data_flags, = struct.unpack('>H', sect[0xF2:0xF4])
print "Extra Data Flags = %d" %extra_data_flags
- if mobi_version <= 5:
- # multibyte utf8 data is included in the encryption for mobi_version 5 and below
+ if mobi_version < 7:
+ # multibyte utf8 data is included in the encryption for mobi_version 6 and below
# so clear that byte so that we leave it to be decrypted.
extra_data_flags &= 0xFFFE
diff --git a/Kindle_Mobi_Tools/Kindle_4_PC_Unswindle/mobidedrm.py b/Kindle_Mobi_Tools/Kindle_4_PC_Unswindle/mobidedrm.py
index 9f17a3b..183432c 100644
--- a/Kindle_Mobi_Tools/Kindle_4_PC_Unswindle/mobidedrm.py
+++ b/Kindle_Mobi_Tools/Kindle_4_PC_Unswindle/mobidedrm.py
@@ -39,8 +39,9 @@
# Removed the disabled Calibre plug-in code
# Permit use of 8-digit PIDs
# 0.19 - It seems that multibyte entries aren't encrypted in a v6 file either.
+# 0.20 - Corretion: It seems that multibyte entries are encrypted in a v6 file.
-__version__ = '0.19'
+__version__ = '0.20'
import sys
import struct
@@ -208,8 +209,8 @@ class DrmStripper:
if (mobi_length >= 0xE4) and (mobi_version >= 5):
extra_data_flags, = struct.unpack('>H', sect[0xF2:0xF4])
print "Extra Data Flags = %d" %extra_data_flags
- if mobi_version <= 5:
- # multibyte utf8 data is included in the encryption for mobi_version 5 and below
+ if mobi_version < 7:
+ # multibyte utf8 data is included in the encryption for mobi_version 6 and below
# so clear that byte so that we leave it to be decrypted.
extra_data_flags &= 0xFFFE
diff --git a/Kindle_Mobi_Tools/MobiDeDRM.py b/Kindle_Mobi_Tools/MobiDeDRM.py
index 9f17a3b..183432c 100644
--- a/Kindle_Mobi_Tools/MobiDeDRM.py
+++ b/Kindle_Mobi_Tools/MobiDeDRM.py
@@ -39,8 +39,9 @@
# Removed the disabled Calibre plug-in code
# Permit use of 8-digit PIDs
# 0.19 - It seems that multibyte entries aren't encrypted in a v6 file either.
+# 0.20 - Corretion: It seems that multibyte entries are encrypted in a v6 file.
-__version__ = '0.19'
+__version__ = '0.20'
import sys
import struct
@@ -208,8 +209,8 @@ class DrmStripper:
if (mobi_length >= 0xE4) and (mobi_version >= 5):
extra_data_flags, = struct.unpack('>H', sect[0xF2:0xF4])
print "Extra Data Flags = %d" %extra_data_flags
- if mobi_version <= 5:
- # multibyte utf8 data is included in the encryption for mobi_version 5 and below
+ if mobi_version < 7:
+ # multibyte utf8 data is included in the encryption for mobi_version 6 and below
# so clear that byte so that we leave it to be decrypted.
extra_data_flags &= 0xFFFE
diff --git a/Mobi_Additional_Tools/lib/mobidedrm.py b/Mobi_Additional_Tools/lib/mobidedrm.py
index 9f17a3b..183432c 100644
--- a/Mobi_Additional_Tools/lib/mobidedrm.py
+++ b/Mobi_Additional_Tools/lib/mobidedrm.py
@@ -39,8 +39,9 @@
# Removed the disabled Calibre plug-in code
# Permit use of 8-digit PIDs
# 0.19 - It seems that multibyte entries aren't encrypted in a v6 file either.
+# 0.20 - Corretion: It seems that multibyte entries are encrypted in a v6 file.
-__version__ = '0.19'
+__version__ = '0.20'
import sys
import struct
@@ -208,8 +209,8 @@ class DrmStripper:
if (mobi_length >= 0xE4) and (mobi_version >= 5):
extra_data_flags, = struct.unpack('>H', sect[0xF2:0xF4])
print "Extra Data Flags = %d" %extra_data_flags
- if mobi_version <= 5:
- # multibyte utf8 data is included in the encryption for mobi_version 5 and below
+ if mobi_version < 7:
+ # multibyte utf8 data is included in the encryption for mobi_version 6 and below
# so clear that byte so that we leave it to be decrypted.
extra_data_flags &= 0xFFFE
diff --git a/Topaz_Tools/FindTopazEbooks.pyw b/Topaz_Tools/FindTopazEbooks.pyw
new file mode 100644
index 0000000..6a0df30
--- /dev/null
+++ b/Topaz_Tools/FindTopazEbooks.pyw
@@ -0,0 +1,216 @@
+#!/usr/bin/env python
+
+# This is a simple tool to identify all Amazon Topaz ebooks in a specific directory.
+# There always seems to be confusion since Topaz books downloaded to K4PC/Mac can have
+# almost any extension (.azw, .azw1, .prc, tpz). While the .azw1 and .tpz extensions
+# are fairly easy to indentify, the others are not (without opening the files in an editor).
+
+# To run the tool with the GUI frontend, just double-click on the 'FindTopazFiles.pyw' file
+# and select the folder where all of the ebooks in question are located. Then click 'Search'.
+# The program will list the file names of the ebooks that are indentified as being Topaz.
+# You can then isolate those books and use the Topaz tools to decrypt and convert them.
+
+# You can also run the script from a command line... supplying the folder to search
+# as a parameter: python FindTopazEbooks.pyw "C:\My Folder" (change appropriately for
+# your particular O.S.)
+
+# ** NOTE: This program does NOT decrypt or modify Topaz files in any way. It simply identifies them.
+
+# PLEASE DO NOT PIRATE EBOOKS!
+
+# We want all authors and publishers, and eBook stores to live
+# long and prosperous lives but at the same time we just want to
+# be able to read OUR books on whatever device we want and to keep
+# readable for a long, long time
+
+# This borrows very heavily from works by CMBDTC, IHeartCabbages, skindle,
+# unswindle, DarkReverser, ApprenticeAlf, DiapDealer, some_updates
+# and many many others
+
+# Revision history:
+# 1 - Initial release.
+
+from __future__ import with_statement
+
+__license__ = 'GPL v3'
+
+import sys
+import os
+import re
+import shutil
+import Tkinter
+import Tkconstants
+import tkFileDialog
+import tkMessageBox
+
+
+class ScrolledText(Tkinter.Text):
+ def __init__(self, master=None, **kw):
+ self.frame = Tkinter.Frame(master)
+ self.vbar = Tkinter.Scrollbar(self.frame)
+ self.vbar.pack(side=Tkconstants.RIGHT, fill=Tkconstants.Y)
+ kw.update({'yscrollcommand': self.vbar.set})
+ Tkinter.Text.__init__(self, self.frame, **kw)
+ self.pack(side=Tkconstants.LEFT, fill=Tkconstants.BOTH, expand=True)
+ self.vbar['command'] = self.yview
+ # Copy geometry methods of self.frame without overriding Text
+ # methods = hack!
+ text_meths = vars(Tkinter.Text).keys()
+ methods = vars(Tkinter.Pack).keys() + vars(Tkinter.Grid).keys() + vars(Tkinter.Place).keys()
+ methods = set(methods).difference(text_meths)
+ for m in methods:
+ if m[0] != '_' and m != 'config' and m != 'configure':
+ setattr(self, m, getattr(self.frame, m))
+
+ def __str__(self):
+ return str(self.frame)
+
+
+def cli_main(argv=sys.argv, obj=None):
+ progname = os.path.basename(argv[0])
+ if len(argv) != 2:
+ print "usage: %s DIRECTORY" % (progname,)
+ return 1
+
+ if obj == None:
+ print "\nTopaz search results:\n"
+ else:
+ obj.stext.insert(Tkconstants.END,"Topaz search results:\n\n")
+
+ inpath = argv[1]
+ files = os.listdir(inpath)
+ filefilter = re.compile("(\.azw$)|(\.azw1$)|(\.prc$)|(\.tpz$)", re.IGNORECASE)
+ files = filter(filefilter.search, files)
+
+ if files:
+ topazcount = 0
+ totalcount = 0
+ for filename in files:
+ with open(os.path.join(inpath, filename), 'rb') as f:
+ try:
+ if f.read().startswith('TPZ'):
+ f.close()
+ basename, extension = os.path.splitext(filename)
+ if obj == None:
+ print " %s is a Topaz formatted ebook." % filename
+ """
+ if extension == '.azw' or extension == '.prc':
+ print " renaming to %s" % (basename + '.tpz')
+ shutil.move(os.path.join(inpath, filename),
+ os.path.join(inpath, basename + '.tpz'))
+ """
+ else:
+ msg1 = " %s is a Topaz formatted ebook.\n" % filename
+ obj.stext.insert(Tkconstants.END,msg1)
+ """
+ if extension == '.azw' or extension == '.prc':
+ msg2 = " renaming to %s\n" % (basename + '.tpz')
+ obj.stext.insert(Tkconstants.END,msg2)
+ shutil.move(os.path.join(inpath, filename),
+ os.path.join(inpath, basename + '.tpz'))
+ """
+ topazcount += 1
+ except:
+ if obj == None:
+ print " Error reading %s." % filename
+ else:
+ msg = " Error reading or %s.\n" % filename
+ obj.stext.insert(Tkconstants.END,msg)
+ pass
+ totalcount += 1
+ if topazcount == 0:
+ if obj == None:
+ print "\nNo Topaz books found in %s." % inpath
+ else:
+ msg = "\nNo Topaz books found in %s.\n\n" % inpath
+ obj.stext.insert(Tkconstants.END,msg)
+ else:
+ if obj == None:
+ print "\n%i Topaz books found in %s\n%i total books checked.\n" % (topazcount, inpath, totalcount)
+ else:
+ msg = "\n%i Topaz books found in %s\n%i total books checked.\n\n" %(topazcount, inpath, totalcount)
+ obj.stext.insert(Tkconstants.END,msg)
+ else:
+ if obj == None:
+ print "No typical Topaz file extensions found in %s.\n" % inpath
+ else:
+ msg = "No typical Topaz file extensions found in %s.\n\n" % inpath
+ obj.stext.insert(Tkconstants.END,msg)
+
+ return 0
+
+
+class DecryptionDialog(Tkinter.Frame):
+ def __init__(self, root):
+ Tkinter.Frame.__init__(self, root, border=5)
+ ltext='Search a directory for Topaz eBooks\n'
+ self.status = Tkinter.Label(self, text=ltext)
+ 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='Directory to Search').grid(row=1)
+ self.inpath = Tkinter.Entry(body, width=30)
+ self.inpath.grid(row=1, column=1, sticky=sticky)
+ button = Tkinter.Button(body, text="...", command=self.get_inpath)
+ button.grid(row=1, column=2)
+ msg1 = 'Topaz search results \n\n'
+ self.stext = ScrolledText(body, bd=5, relief=Tkconstants.RIDGE,
+ height=15, width=60, wrap=Tkconstants.WORD)
+ self.stext.grid(row=4, column=0, columnspan=2,sticky=sticky)
+ #self.stext.insert(Tkconstants.END,msg1)
+ buttons = Tkinter.Frame(self)
+ buttons.pack()
+
+
+ self.botton = Tkinter.Button(
+ buttons, text="Search", width=10, command=self.search)
+ self.botton.pack(side=Tkconstants.LEFT)
+ Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
+ self.button = Tkinter.Button(
+ buttons, text="Quit", width=10, command=self.quit)
+ self.button.pack(side=Tkconstants.RIGHT)
+
+ def get_inpath(self):
+ cwd = os.getcwdu()
+ cwd = cwd.encode('utf-8')
+ inpath = tkFileDialog.askdirectory(
+ parent=None, title='Directory to search',
+ initialdir=cwd, initialfile=None)
+ if inpath:
+ inpath = os.path.normpath(inpath)
+ self.inpath.delete(0, Tkconstants.END)
+ self.inpath.insert(0, inpath)
+ return
+
+
+ def search(self):
+ inpath = self.inpath.get()
+ if not inpath or not os.path.exists(inpath):
+ self.status['text'] = 'Specified directory does not exist'
+ return
+ argv = [sys.argv[0], inpath]
+ self.status['text'] = 'Searching...'
+ self.botton.configure(state='disabled')
+ cli_main(argv, self)
+ self.status['text'] = 'Search a directory for Topaz files'
+ self.botton.configure(state='normal')
+
+ return
+
+
+def gui_main():
+ root = Tkinter.Tk()
+ root.title('Topaz eBook Finder')
+ root.resizable(True, False)
+ root.minsize(370, 0)
+ DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1)
+ root.mainloop()
+ return 0
+
+
+if __name__ == '__main__':
+ if len(sys.argv) > 1:
+ sys.exit(cli_main())
+ sys.exit(gui_main())
\ No newline at end of file