*[kindle]kindle unpack(mobi unpack)

amazonkindle用書籍を作成するツールを提供している
GUIKindle Previewer 3とCUIのkindlegen

生成されるmobiファイルには古いkindle端末でも読める旧フォーマット、
今の端末用の新フォーマット、そしてmobiを作成する際に使用した
ソースファイルが格納(注)される
(注)別途記事に書くがkindlegenでソースファイルを格納しない方法がある

このためファイルサイズが大きくなるのだが、自分に不要なデータを
削除してダイエットするのがKindleUnpack(旧名mobiUnpack)

もとは.mobilereadのフォーラムが発祥
https://www.mobileread.com/forums/showthread.php?t=61986

現在はgithubリポジトリ管理されている
https://github.com/kevinhendricks/KindleUnpack

KindleUnpackにはGUI用とCUI用が用意されているが、windows環境では
コードページの関係でCUI用がちゃんと動かない
このためGUI用に手を加えて引数で処理ファイル名を指定できるようにした

Pythonは勉強を始めたばかりなのでもっと最適化できるのではないか思う<使い方> ※.pywファイルにはpythonw.exeを関連付け済

KindleUnpack.pyw ARG1 ARG2 ARG3

ARG1 変換元ファイル名(ファイルの存在チェックはなし)
ARG2 変換後ファイル出力先(出力先パスの妥当性チェックはなし)
ARG3 変換処理後に自動終了指示(存在チェックのみなのでなんでもよい)

KindleUnpack.pyw

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab

from __future__ import unicode_literals, division, absolute_import, print_function

import sys

#引数で処理対象ファイルを指定できるように修正
argvs = sys.argv
argvc = len(argvs)

from lib.compatibility_utils import PY2, text_type, unicode_str
from lib.compatibility_utils import unicode_argv, add_cp65001_codec

import lib.unipath as unipath
from lib.unipath import pathof

import os
import traceback

import codecs
add_cp65001_codec()

try:
    from queue import Full
    from queue import Empty
except ImportError:
    from Queue import Full
    from Queue import Empty

if PY2 and sys.platform.startswith("win"):
    from libgui.askfolder_ed import AskFolder

from multiprocessing import Process, Queue

if PY2:
    import Tkinter as tkinter
    import Tkconstants as tkinter_constants
    import tkFileDialog as tkinter_filedialog
    import ttk as tkinter_ttk
else:
    import tkinter
    import tkinter.constants as tkinter_constants
    import tkinter.filedialog as tkinter_filedialog
    import tkinter.ttk as tkinter_ttk

from libgui.scrolltextwidget import ScrolledText

import lib.kindleunpack as kindleunpack

# 各種設定を.jsonファイルに保存しプログラム開始時に読み込みます
# 設定を.jsonファイルに保存しない場合は、falseに設定します
# falseに設定しても.jsonファイルの削除は自動で行いません
# 手動で削除してください
PERSISTENT_PREFS = True

from inspect import getfile, currentframe
from libgui.prefs import getprefs, saveprefs

# おそらく過剰ですがスクリプトがどのプラットフォームで呼び出されても大丈夫なように念のため
SCRIPT_NAME = unicode_str(getfile(currentframe()))
SCRIPT_DIR = unicode_str(os.path.dirname(unipath.abspath(getfile(currentframe()))))
PROGNAME = unicode_str(os.path.splitext(SCRIPT_NAME)[0])

# .jsonファイル名にプラットフォームを含めることで設定が引き継がれます
# 異なるOSがネットワーク共有/フラッシュドライブを介して同じスクリプトにアクセスする場合に発生します
CONFIGFILE = unicode_str(os.path.join(SCRIPT_DIR, '{0}_{1}.json'.format(PROGNAME, sys.platform[:3])))

# 出力が共有キューに追加されるようにストリームをラップする
# utf-8エンコーディングを使用
class QueuedStream:
    def __init__(self, stream, q):
        self.stream = stream
        self.encoding = stream.encoding
        self.q = q
        if self.encoding == None:
            self.encoding = 'utf-8'
    def write(self, data):
        if isinstance(data,text_type):
            data = data.encode('utf-8')
        elif self.encoding not in ['utf-8','UTF-8','cp65001','CP65001']:
            udata = data.decode(self.encoding)
            data = udata.encode('utf-8')
        self.q.put(data)
    def __getattr__(self, attr):
        if attr == 'mode':
            return 'wb'
        if attr == 'encoding':
            return 'utf-8'
        return getattr(self.stream, attr)


class MainDialog(tkinter.Frame):

    def __init__(self, root):
        tkinter.Frame.__init__(self, root, border=5)
        self.root = root
        self.interval = 50
        self.p2 = None
        self.q = Queue()
        # 今後の設定の追加/削除を容易に行うために:
        # フォーマットを守ろう - TK Widget name = prefs dictionary key = ini.get|set name.
        # 例: mobipath = prefs['mobipath'] = config.get('Defaults', mobipath).
        self.prefs = getprefs(CONFIGFILE, self.root, PERSISTENT_PREFS)

        self.status = tkinter.StringVar()
        tkinter.Label(self, textvariable=self.status, justify='center').grid(row=0, columnspan=3, sticky=tkinter_constants.N)
        self.status.set('Upack a non-DRM Kindle eBook')
        sticky = tkinter_constants.E + tkinter_constants.W
        ALL = tkinter_constants.E+tkinter_constants.W+tkinter_constants.N+tkinter_constants.S
        # テキストボックスに初期値を設定します。
        self.grid_columnconfigure(1, weight=1)
        # デバッグログを設定します。
        self.grid_rowconfigure(10, weight=1)

        tkinter.Label(self, text='').grid(row=1, sticky=tkinter_constants.E)
        tkinter.Label(self, text='Unencrypted Kindle eBook input file', wraplength=200).grid(row=2, sticky=tkinter_constants.E)
        self.mobipath = tkinter.Entry(self, width=50)
        self.mobipath.grid(row=2, column=1, sticky=sticky)

        #引数指定がある場合に処理対象ファイルをセット
        #SJISのままだと文字化けするのでUNICODEに変換
        if (argvc > 1):
            self.mobipath.insert(0,unicode(argvs[1], 'cp932'))
        else:
            self.mobipath.insert(0, '')

        button = tkinter.Button(self, text="Browse...", command=self.get_mobipath)
        button.grid(row=2, column=2, sticky=sticky)

        tkinter.Label(self, text='Output Directory', wraplength=200).grid(row=3, sticky=tkinter_constants.E)
        self.outpath = tkinter.Entry(self, width=50)
        self.outpath.grid(row=3, column=1, sticky=sticky)

        #引数指定がある場合に出力フォルダをセット
        #SJISのままだと文字化けするのでUNICODEに変換
        if (argvc > 2):
            outpath = pathof(os.path.normpath(unicode(argvs[2], 'cp932')))
            self.outpath.insert(0, outpath)
        else:
            if self.prefs['outpath'] and PERSISTENT_PREFS and unipath.exists(CONFIGFILE):
                outpath = pathof(os.path.normpath(self.prefs['outpath']))
                self.outpath.insert(0, outpath)
            else:
                self.outpath.insert(0, '')
        button = tkinter.Button(self, text="Browse...", command=self.get_outpath)
        button.grid(row=3, column=2, sticky=sticky)

        tkinter.Label(self, text='OPTIONAL: APNX file Associated with AZW3', wraplength=200).grid(row=4, sticky=tkinter_constants.E)
        self.apnxpath = tkinter.Entry(self, width=50)
        self.apnxpath.grid(row=4, column=1, sticky=sticky)
        self.apnxpath.insert(0, '')
        button = tkinter.Button(self, text="Browse...", command=self.get_apnxpath)
        button.grid(row=4, column=2, sticky=sticky)

        self.splitvar = tkinter.IntVar()
        checkbox = tkinter.Checkbutton(self, text="Split Combination Kindlegen eBooks", variable=self.splitvar)
        if self.prefs['splitvar'] and PERSISTENT_PREFS:
            checkbox.select()
        checkbox.grid(row=5, column=1, columnspan=2, sticky=tkinter_constants.W)

        self.rawvar = tkinter.IntVar()
        checkbox = tkinter.Checkbutton(self, text="Write Raw Data", variable=self.rawvar)
        if self.prefs['rawvar'] and PERSISTENT_PREFS:
            checkbox.select()
        checkbox.grid(row=6, column=1, columnspan=2, sticky=tkinter_constants.W)

        self.dbgvar = tkinter.IntVar()
        checkbox = tkinter.Checkbutton(self, text="Dump Mode", variable=self.dbgvar)
        if self.prefs['dbgvar'] and PERSISTENT_PREFS:
            checkbox.select()
        checkbox.grid(row=7, column=1, columnspan=2, sticky=tkinter_constants.W)

        self.hdvar = tkinter.IntVar()
        checkbox = tkinter.Checkbutton(self, text="Use HD Images If Present", variable=self.hdvar)
        if self.prefs['hdvar'] and PERSISTENT_PREFS:
            checkbox.select()
        checkbox.grid(row=8, column=1, columnspan=2, sticky=tkinter_constants.W)

        tkinter.Label(self, text='ePub Output Type:').grid(row=9, sticky=tkinter_constants.E)
        self.epubver_val = tkinter.StringVar()
        self.epubver = tkinter_ttk.Combobox(self, textvariable=self.epubver_val, state='readonly')
        self.epubver['values'] = ('ePub 2', 'ePub 3', 'Auto-detect', 'Force ePub 2')
        self.epubver.current(0)
        if self.prefs['epubver'] and PERSISTENT_PREFS:
            self.epubver.current(self.prefs['epubver'])
        self.epubver.grid(row=9, column=1, columnspan=2, pady=(3,5), sticky=tkinter_constants.W)

        msg1 = 'Conversion Log \n\n'
        self.stext = ScrolledText(self, bd=5, relief=tkinter_constants.RIDGE, wrap=tkinter_constants.WORD)
        self.stext.grid(row=10, column=0, columnspan=3, sticky=ALL)
        self.stext.insert(tkinter_constants.END,msg1)

        self.sbotton = tkinter.Button(
            self, text="Start", width=10, command=self.convertit)
        self.sbotton.grid(row=11, column=1, sticky=tkinter_constants.S+tkinter_constants.E)

        #引数が指定されている場合にはStartボタンにフォーカス移動
        if (argvc > 1):
            self.sbotton.focus_set()

        #Returnを割り当てるとなぜかそのまま処理に入る
        self.sbotton.bind('<Return>',self.convertit())

        self.qbutton = tkinter.Button(
            self, text="Quit", width=10, command=self.quitting)
        self.qbutton.grid(row=11, column=2, sticky=tkinter_constants.S+tkinter_constants.W)
        if self.prefs['windowgeometry'] and PERSISTENT_PREFS:
            self.root.geometry(self.prefs['windowgeometry'])
        else:
            self.root.update_idletasks()
            w = self.root.winfo_screenwidth()
            h = self.root.winfo_screenheight()
            rootsize = (605, 575)
            x = w//2 - rootsize[0]//2
            y = h//2 - rootsize[1]//2
            self.root.geometry('%dx%d+%d+%d' % (rootsize + (x, y)))
        self.root.protocol('WM_DELETE_WINDOW', self.quitting)

    # このメインプロセスと生成された子プロセスとの間で共有されるキューの読み取り
    def readQueueUntilEmpty(self):
        done = False
        text = ''
        while not done:
            try:
                data = self.q.get_nowait()
                text += unicode_str(data, 'utf-8')
            except Empty:
                done = True
                pass
        return text

    # ブロックせずにサブプロセスパイプから読み取る
    # ウィジェット "after"を介して間隔ごとに呼び出されます
    # オプションが使用されているため、次回リセットする必要があります
    def processQueue(self):
        poll = self.p2.exitcode
        if poll != None:
            text = self.readQueueUntilEmpty()
            msg = text + '\n\n' + 'eBook successfully unpacked\n'
            if poll != 0:
                msg = text + '\n\n' + 'Error: Unpacking Failed\n'
            self.p2.join()
            self.showCmdOutput(msg)
            self.p2 = None
            self.sbotton.configure(state='normal')
            #展開後、Quitボタンにフォーカス移動
            self.qbutton.focus_set()

            if (argvc > 3):
                #Returnを割り当てるとなぜかそのまま処理に入る            
                self.sbotton.bind('<Return>',self.quitting())

            return
        text = self.readQueueUntilEmpty()
        self.showCmdOutput(text)
        # make sure we get invoked again by event loop after interval
        self.stext.after(self.interval,self.processQueue)
        return

    # スクロールされたテキストウィジェットのサブプロセスからの出力をポストする
    def showCmdOutput(self, msg):
        if msg and msg !='':
            if sys.platform.startswith('win'):
                msg = msg.replace('\r\n','\n')
            self.stext.insert(tkinter_constants.END,msg)
            self.stext.yview_pickplace(tkinter_constants.END)
        return

    def get_mobipath(self):
        cwd = unipath.getcwd()
        mobipath = tkinter_filedialog.askopenfilename(
            parent=None, title='Select Unencrypted Kindle eBook File',
            initialdir=self.prefs['mobipath'] or cwd,
            initialfile=None,
            defaultextension=('.mobi', '.prc', '.azw', '.azw4', '.azw3'),
            filetypes=[('All Kindle formats', ('.mobi', '.prc', '.azw', '.azw4', '.azw3')),
                       ('Kindle Mobi eBook File', '.mobi'), ('Kindle PRC eBook File', '.prc'),
                       ('Kindle AZW eBook File', '.azw'), ('Kindle AZW4 Print Replica', '.azw4'),
                       ('Kindle Version 8', '.azw3'),('All Files', '.*')])
        if mobipath:
            self.prefs['mobipath'] = pathof(os.path.dirname(mobipath))
            mobipath = pathof(os.path.normpath(mobipath))
            self.mobipath.delete(0, tkinter_constants.END)
            self.mobipath.insert(0, mobipath)
        return

    def get_apnxpath(self):
        cwd = unipath.getcwd()
        apnxpath = tkinter_filedialog.askopenfilename(
            parent=None, title='Optional APNX file associated with AZW3',
            initialdir=self.prefs['apnxpath'] or cwd,
            initialfile=None,
            defaultextension='.apnx', filetypes=[('Kindle APNX Page Information File', '.apnx'), ('All Files', '.*')])
        if apnxpath:
            self.prefs['apnxpath'] = pathof(os.path.dirname(apnxpath))
            apnxpath = pathof(os.path.normpath(apnxpath))
            self.apnxpath.delete(0, tkinter_constants.END)
            self.apnxpath.insert(0, apnxpath)
        return

    def get_outpath(self):
        cwd = unipath.getcwd()
        if sys.platform.startswith("win") and PY2:
            # 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="Folder to Store Output into",
                defaultLocation=self.prefs['outpath'] or unipath.getcwd())
        else:
            outpath = tkinter_filedialog.askdirectory(
                parent=None, title='Folder to Store Output into',
                initialdir=self.prefs['outpath'] or cwd, initialfile=None)
        if outpath:
            self.prefs['outpath'] = outpath
            outpath = pathof(os.path.normpath(outpath))
            self.outpath.delete(0, tkinter_constants.END)
            self.outpath.insert(0, outpath)
        return

    # 終了処理
    def quitting(self):
        # 実行中のサブプロセスを強制終了する
        if self.p2 != None:
            if (self.p2.exitcode == None):
                self.p2.terminate()
        if PERSISTENT_PREFS:
            # .jsonに書き出し
            if not saveprefs(CONFIGFILE, self.prefs, self):
                print("Couldn't save INI file.")
        self.root.destroy()
        self.quit()

    # 展開処理(子プロセスで実行)
    def convertit(self):
        # 多重起動を防ぐためボタンを無効にする
        self.sbotton.configure(state='disabled')
        mobipath = unicode_str(self.mobipath.get())
        apnxpath = unicode_str(self.apnxpath.get())
        outdir = unicode_str(self.outpath.get())
        if not mobipath or not unipath.exists(mobipath):
            self.status.set('Specified eBook file does not exist')
            self.sbotton.configure(state='normal')
            return
        apnxfile = None
        if apnxpath != "" and unipath.exists(apnxpath):
            apnxfile = apnxpath
        if not outdir:
            self.status.set('No output directory specified')
            self.sbotton.configure(state='normal')
            return
        q = self.q
        log = 'Input Path = "'+ mobipath + '"\n'
        log += 'Output Path = "' + outdir + '"\n'
        if apnxfile != None:
            log += 'APNX Path = "' + apnxfile + '"\n'
        dump = False
        writeraw = False
        splitcombos = False
        use_hd = False
        if self.dbgvar.get() == 1:
            dump = True
            log += 'Debug = True\n'
        if self.rawvar.get() == 1:
            writeraw = True
            log += 'WriteRawML = True\n'
        if self.splitvar.get() == 1:
            splitcombos = True
            log += 'Split Combo KF8 Kindle eBooks = True\n'
        if self.epubver.current() == 0:
            epubversion = '2'
        elif self.epubver.current() == 1:
            epubversion = '3'
        elif self.epubver.current() == 2:
            epubversion = 'A'
        else:
            epubversion = 'F'
        log += 'Epub Output Type Set To: {0}\n'.format(self.epubver_val.get())
        if self.hdvar.get():
            use_hd = True
            # stub for processing the Use HD Images setting
            log += 'Use HD Images If Present = True\n'
        log += '\n\n'
        log += 'Please Wait ...\n\n'
        self.stext.insert(tkinter_constants.END,log)
        self.p2 = Process(target=unpackEbook, args=(q, mobipath, outdir, apnxfile, epubversion, use_hd, dump, writeraw, splitcombos))
        self.p2.start()

        # Pythonはあなたが他のすべてのGUIが行う独自のイベントループを
        # 作成することを許可していないようです
        # 仕方がないのでウィジェット "after"を使用して
        # non-guiイベントを実行するイベント・ループを強制起動する
        self.stext.after(self.interval,self.processQueue)
        return


# 子プロセス/マルチプロセッシングスレッドはここから開始します
def unpackEbook(q, infile, outdir, apnxfile, epubversion, use_hd, dump, writeraw, splitcombos):
    sys.stdout = QueuedStream(sys.stdout, q)
    sys.stderr = QueuedStream(sys.stderr, q)
    rv = 0
    try:
        kindleunpack.unpackBook(infile, outdir, apnxfile, epubversion, use_hd, dodump=dump, dowriteraw=writeraw, dosplitcombos=splitcombos)
    except Exception as e:
        print("Error: %s" % e)
        print(traceback.format_exc())
        rv = 1
    sys.exit(rv)


def main(argv=unicode_argv()):
    root = tkinter.Tk()
    root.title('Kindle eBook Unpack Tool')
    root.minsize(440, 350)
    root.resizable(True, True)
    MainDialog(root).pack(fill=tkinter_constants.BOTH, expand=tkinter_constants.YES)
    root.mainloop()
    return 0

if __name__ == "__main__":
    sys.exit(main())

*[雑記]amazonプライムデー 2018

kindle本メインだとプライム会員の登録はあまりメリットが無い

kindle本には発送が無いからお急ぎ便も関係がない
端末のセールではプライム会員限定価格が設定されることが多いが
端末はそうそう何度も購入する物では無いし、プライム会員限定の
Prime Reading は同種のサービス Kindle Unlimited より対象範囲が狭い
(Kindle Unlimited は月額980円とちょっと高いのが難点)
と言うことで以前は利用していたが今はプライム会員の登録をしていない

そのプライム会員をターゲットにした年に一度のプライムデー
今年は7/16 12:00開始だが・・・興味を引く物はなし
そう言えば去年も同じ感想だった(苦笑)
http://d.hatena.ne.jp/nakapon/20170710

kindle本の購入にメリットがあれば加入するんだがなぁ<追記>7/17
後数時間でやっと終わる
5chの評判もあまり良くない
「在庫整理だ」「プライムデー前に(割引に備えて)価格が上がった」など文句が多い