diff --git a/FixCover.py b/FixCover.py index 18d6b30..488661e 100644 --- a/FixCover.py +++ b/FixCover.py @@ -1,14 +1,9 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - import os import re import sys import time import glob -import imghdr import string -import tempfile import sqlite3 from pathlib import Path @@ -19,10 +14,9 @@ class FixCover: name = 'Fix Kindle Ebook Cover' version = '1.2' feedback = 'https://bookfere.com/post/994.html' - description = '%s - v%s\nA tool to fix damaged Kindle ebook covers.\n \ + description = '%s - v%s\nA tool to fix damaged Kindle ebook covers.\n\ Feedback: %s' % (name, version, feedback) - def __init__(self, logger=None, progress=None, db=None): self.logger = logger self.progress = progress @@ -43,24 +37,20 @@ def __init__(self, logger=None, progress=None, db=None): self.log('Time: %s' % time.strftime('%Y-%m-%d %H:%M:%S')) self.log(self.description, True) - def log(self, text, sep=False): if self.logger is not None: divider = '-------------------------------------------' - text = '%s\n%s\n%s' % (divider, text, divider) \ + text = '%s\n%s\n%s' % (divider, text, divider) \ if sep is True else text self.logger(text) - def print_progress(self, factor): if self.progress is not None: self.progress(factor) - def get_filepath_list(self, path): return glob.glob('%s%s**' % (path, os.sep), recursive=True) - def get_ebook_thumbnails_via_path(self, path): thumbnails = dict() for thumbnail in self.get_filepath_list(path): @@ -72,7 +62,6 @@ def get_ebook_thumbnails_via_path(self, path): thumbnails[asin.group(1)] = thumbnail return thumbnails - def get_ebook_thumbnails_via_db(self): # self.db_cursor.row_factory = lambda cursor, row: row[0] thumbnails = self.db_cursor.execute(' \ @@ -81,14 +70,12 @@ def get_ebook_thumbnails_via_db(self): AND p_location IS NOT NULL') return [row[0] for row in thumbnails.fetchall()] - def is_damaged_thumbnail(self, path): try: return os.path.getsize(path) < 2000 - except: + except Exception: return False - def get_damaged_thumbnails(self, path): thumbnails = self.get_ebook_thumbnails_via_path(path) for thumbnail in thumbnails.copy(): @@ -97,14 +84,12 @@ def get_damaged_thumbnails(self, path): del thumbnails[thumbnail] return thumbnails - def is_valid_ebook_file(self, filename): for ext in ['.mobi', '.azw', '.azw3', 'azw4']: if filename.endswith(ext): return True return False - def get_ebook_list_via_path(self, path): ebook_list = [] for filename in self.get_filepath_list(path): @@ -114,16 +99,14 @@ def get_ebook_list_via_path(self, path): ebook_list.append(filename) return ebook_list - def get_ebook_asisn_from_filename(self, filename): asin = re.search( - '_([A-Z0-9]{10})\.(?:kfx|azw\d{0,1}|prc|[mp]obi)$', + r'_([\w-]*)\.(?:kfx|azw\d{0,1}|prc|[mp]obi)$', filename ) if asin is not None: self.guessed_asins.append(asin.group(1)) - def get_ebook_list_via_db(self): ebook_list = self.db_cursor.execute(" \ SELECT p_uuid, p_location, p_thumbnail, p_cdeType FROM Entries \ @@ -131,32 +114,26 @@ def get_ebook_list_via_db(self): AND p_location IS NOT NULL") return ebook_list.fetchall() - def store_ebook_thumbnail(self, path, data): with open(path, 'wb') as file: file.write(data) - def get_ebook_metadata(self, path): - ebook_asin = None - ebook_type = None - ebook_cover = None + asin = cdetype = cover = None try: mobi_file = MOBIFile(path) - ebook_asin = mobi_file.get_metadata('ASIN') - ebook_type = mobi_file.get_metadata('Document Type') - ebook_cover = mobi_file.get_cover_image() - except: + asin = mobi_file.get_metadata('ASIN') + cdetype = mobi_file.get_metadata('Document Type') + cover = mobi_file.get_cover_image() + except Exception: pass - return (ebook_asin, ebook_type, ebook_cover) - + return (asin, cdetype, cover) def get_thumbnail_name(self, asin, cdetype): return 'thumbnail_%s_%s_portrait.jpg' % (asin, cdetype) - def fix_via_db(self, thumbnails_path): for row in self.get_ebook_list_via_db(): p_uuid, p_location, p_thumbnail, p_cde = row @@ -166,75 +143,83 @@ def fix_via_db(self, thumbnails_path): asin, cde, cover = self.get_ebook_metadata(p_location) if p_location.endswith('KUAL.kual'): - cover = Path(os.path.join(os.path.dirname(__file__), - 'kual.jpg')).read_bytes() + cover = Path( + os.path.join(os.path.dirname(__file__), 'kual.jpg') + ).read_bytes() elif not self.is_valid_ebook_file(p_location): continue elif cover is None: - self.failure_jobs['ebook_errors'].append('%s\n └─[%s] %s' % - ('No cover was found.', p_cde, Path(p_location).name)) + self.failure_jobs['ebook_errors'].append( + '%s\n └─[%s] %s' % + ('No cover was found.', p_cde, Path(p_location).name) + ) continue if p_thumbnail is None and p_cde in ('EBOK', 'PDOC'): asin = asin if asin is not None else p_uuid cde = cde if cde is not None else p_cde - thumbnail_path = os.path.join(thumbnails_path, - self.get_thumbnail_name(asin, cde)) + thumbnail_path = os.path.join( + thumbnails_path, + self.get_thumbnail_name(asin, cde) + ) self.store_ebook_thumbnail(thumbnail_path, cover) self.db_cursor.execute('UPDATE Entries SET p_thumbnail = ? \ WHERE p_location = ?', (thumbnail_path, p_location)) - self.log('✓ Generated: %s\n └─[%s] %s' % - (Path(thumbnail_path).name, p_cde, Path(p_location).name)) - elif p_thumbnail is not None and (not os.path.exists(p_thumbnail) - or self.is_damaged_thumbnail(p_thumbnail)): - self.store_ebook_thumbnail(p_thumbnail, cover) - self.log('✓ Fixed: %s\n └─[%s] %s' % - (Path(p_thumbnail).name, p_cde, Path(p_location).name)) - + self.log( + '✓ Generated: %s\n └─[%s] %s' % + (Path(thumbnail_path).name, p_cde, Path(p_location).name) + ) + elif p_thumbnail is not None and ( + not os.path.exists(p_thumbnail) + or self.is_damaged_thumbnail(p_thumbnail) + ): + self.store_ebook_thumbnail(p_thumbnail, cover) + self.log( + '✓ Fixed: %s\n └─[%s] %s' % + (Path(p_thumbnail).name, p_cde, Path(p_location).name) + ) def fix_via_path(self, thumbnails, documents_path, thumbnails_path): ebook_list = self.get_ebook_list_via_path(documents_path) for ebook in ebook_list: self.print_progress(len(ebook_list)) - ebook_asin, ebook_type, ebook_cover = self.get_ebook_metadata(ebook) + asin, cdetype, cover = self.get_ebook_metadata(ebook) ebook = Path(ebook) - if ebook_cover is None: - self.failure_jobs['ebook_errors'].append('%s\n └─[%s] %s' % - ('No cover was found.', ebook_type, ebook.name)) + if cover is None: + self.failure_jobs['ebook_errors'].append( + '%s\n └─[%s] %s' % + ('No cover was found.', cdetype, ebook.name) + ) continue - if ebook_type == 'EBOK' and ebook_asin in thumbnails.keys(): - thumbnail_path = thumbnails[ebook_asin] + if cdetype == 'EBOK' and asin in thumbnails.keys(): + thumbnail_path = thumbnails[asin] thumbnail_name = Path(thumbnail_path).name - self.store_ebook_thumbnail(thumbnail_path, ebook_cover) - self.log('✓ Fixed: %s\n └─[%s] %s' % - (thumbnail_name, ebook_type, ebook.name)) + self.store_ebook_thumbnail(thumbnail_path, cover) + self.log( + '✓ Fixed: %s\n └─[%s] %s' % + (thumbnail_name, cdetype, ebook.name) + ) self.conquest_jobs += 1 - del thumbnails[ebook_asin] - elif ebook_type == 'EBOK' and ebook_asin is not None: - thumbnail_name = self.get_thumbnail_name(ebook_asin, ebook_type) + del thumbnails[asin] + elif cdetype == 'EBOK' and asin is not None: + thumbnail_name = self.get_thumbnail_name(asin, cdetype) thumbnail_path = os.path.join(thumbnails_path, thumbnail_name) if not os.path.exists(thumbnail_path): - self.store_ebook_thumbnail(thumbnail_path, ebook_cover) - self.log('✓ Generated: %s\n └─[%s] %s' % - (thumbnail_name, ebook_type, ebook.name)) + self.store_ebook_thumbnail(thumbnail_path, cover) + self.log( + '✓ Generated: %s\n └─[%s] %s' % + (thumbnail_name, cdetype, ebook.name) + ) self.conquest_jobs += 1 - # [BUG] Do this will make Kindle can not open ebook. - # elif ebook_type == 'PDOC' and ebook.suffix == '.azw3': - # target = ebook.with_suffix('.mobi') - # ebook.rename(target) - # self.log( - # '✓ Rename %s -> %s to show cover.\n └─[%s] %s' % - # (ebook.suffix, target.suffix, ebook_type, target.name) - # ) - - self.failure_jobs['cover_errors'] = [Path(thumbnail).name for - thumbnail in thumbnails.values()] - self.print_progress(0) + self.failure_jobs['cover_errors'] = [ + Path(thumbnail).name for thumbnail in thumbnails.values() + ] + self.print_progress(0) def fix_ebook_thumbnails(self, documents_path, thumbnails_path): self.log('Checking damaged ebook covers:', True) @@ -281,7 +266,6 @@ def fix_ebook_thumbnails(self, documents_path, thumbnails_path): else: self.log('- No ebook cover need to fix.') - def clean_orphan_thumbnails(self, documents_path, thumbnails_path): self.log('Analysing orphan ebook covers:', True) @@ -291,18 +275,21 @@ def clean_orphan_thumbnails(self, documents_path, thumbnails_path): thumbnails = set(thumbnails.values()) \ - set(self.get_ebook_thumbnails_via_db()) else: - ebook_list = self.get_ebook_list_via_path(documents_path) - for ebook in ebook_list: - self.print_progress(len(ebook_list)) - ebook_asin, ebook_type, ebook_cover = self.get_ebook_metadata(ebook) - if ebook_type == 'EBOK' and ebook_asin in thumbnails.keys(): - del thumbnails[ebook_asin] - - for asin in self.guessed_asins: - if asin in thumbnails.keys(): - del thumbnails[asin] - - thumbnails = thumbnails.values() + self.log( + 'This feature Removed due to impossible to delete the orphan' + 'thumbnails perfectly.' + ) + return + # ebook_list = self.get_ebook_list_via_path(documents_path) + # for ebook in ebook_list: + # self.print_progress(len(ebook_list)) + # asin, cdetype, cover = self.get_ebook_metadata(ebook) + # if cdetype == 'EBOK' and asin in thumbnails.keys(): + # del thumbnails[asin] + # for asin in self.guessed_asins: + # if asin in thumbnails.keys(): + # del thumbnails[asin] + # thumbnails = thumbnails.values() self.print_progress(0) @@ -317,21 +304,18 @@ def clean_orphan_thumbnails(self, documents_path, thumbnails_path): self.log('✓ All orphan ebook covers deleted.') - def get_kindle_path(self, path): return ( os.path.join(path, 'documents'), os.path.join(path, 'system', 'thumbnails') ) - def is_kindle_root(self, path): for path in self.get_kindle_path(path): if os.path.exists(path) is False: return False return True - def get_kindle_root_automatically(self): drives = [] roots = [] @@ -349,7 +333,6 @@ def get_kindle_root_automatically(self): return roots - # fix|clean def handle(self, action='fix', roots=[]): if not sys.version_info >= (3, 5): @@ -360,7 +343,8 @@ def handle(self, action='fix', roots=[]): return roots = [roots] if type(roots) != list else roots - roots = self.get_kindle_root_automatically() if len(roots) < 1 else roots + roots = self.get_kindle_root_automatically() \ + if len(roots) < 1 else roots if len(roots) < 1: self.log('You need choose a Kindle root directory first.') @@ -388,8 +372,6 @@ def handle(self, action='fix', roots=[]): return self.log('All jobs done.', True) - self.log('\n') - def __del__(self): if (self.db_access): diff --git a/fix_kindle_ebook_cover.py b/fix_kindle_ebook_cover.py index e858e51..ed80a4d 100755 --- a/fix_kindle_ebook_cover.py +++ b/fix_kindle_ebook_cover.py @@ -8,15 +8,23 @@ if __name__ == "__main__": - parser = argparse.ArgumentParser(description=FixCover.description, - formatter_class=argparse.RawTextHelpFormatter) - parser.add_argument('path', metavar='N', nargs='*', default=[], - help='Kindle root directories (optional)') - parser.add_argument('-a', '--action', dest='action', + parser = argparse.ArgumentParser( + description=FixCover.description, + formatter_class=argparse.RawTextHelpFormatter + ) + parser.add_argument( + 'path', metavar='N', nargs='*', default=[], + help='Kindle root directories (optional)', + ) + parser.add_argument( + '-a', '--action', dest='action', default='fix', choices=['fix', 'clean'], - help='Specify an action to process ebook cover (default: fix)') - parser.add_argument('-d', '--db', dest='database', - default=None, help='Specify a sqlite3 database file.') + help='Specify an action to process ebook cover (default: fix)', + ) + parser.add_argument( + '-d', '--db', dest='database', + default=None, help='Specify a sqlite3 database file.', + ) args = parser.parse_args() diff --git a/fix_kindle_ebook_cover_gui.pyw b/fix_kindle_ebook_cover_gui.pyw index cfbbbc2..e035e87 100644 --- a/fix_kindle_ebook_cover_gui.pyw +++ b/fix_kindle_ebook_cover_gui.pyw @@ -1,33 +1,29 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - # Tkinter API Reference: https://tkdocs.com/pyref/index.html +import sys import threading -from functools import partial -from math import ceil -from tkinter import * -from tkinter import ttk -from tkinter import filedialog -from tkinter import messagebox -from tkinter import scrolledtext +from tkinter import ( + Tk, StringVar, ttk, filedialog, scrolledtext, DISABLED, NORMAL, END, E, W +) from FixCover import FixCover class Application(ttk.Frame): - def __init__(self, master=None): - ttk.Frame.__init__(self, master, padding=10) + def __init__(self): + root = Tk() + super().__init__(root, padding=10) self.entryvalue = StringVar(self) self.grid() self.create_widgets() self.layout_widgets() + self.bind_action() self.fixcover = FixCover( - logger=self.get_logger(), - progress=self.get_progress() + logger=self.get_logger, + progress=self.get_progress, ) roots = self.fixcover.get_kindle_root_automatically() @@ -38,7 +34,6 @@ class Application(ttk.Frame): else: self.insert_log('No Kindle root directory detected.') - def get_kindle_root(self): self.entry.unbind('') self.entry.bind('', lambda e: self.prevent_dbclick_twice()) @@ -47,83 +42,84 @@ class Application(ttk.Frame): if value != '': self.entryvalue.set(value) - def prevent_dbclick_twice(self): self.entry.unbind('') self.entry.bind('', lambda e: self.get_kindle_root()) - def reset_kindle_root(self): self.entryvalue.set(self.entry.get()) - - def get_logger(self): - def logger(text): - self.insert_log(text) - self.log.see(END) - return logger - + def get_logger(self, text): + self.insert_log(text) + self.log.see(END) def insert_log(self, text): self.log.insert(END, '%s\n' % text) + def get_progress(self, factor): + if factor == 0: + self.progress['value'] = 100 + return + self.progress.step(float(str(100 / factor)[:3])) - def get_progress(self): - self.progress['value'] = 0 - def progress(factor): - if factor == 0: - self.progress['value'] = 100 - return - self.progress.step(float(str(100 / factor)[:3])) - return progress - - - def handle_ebook_cover(self, action): + def handle_ebook_cover(self, action='fix'): self.log.delete(1.0, END) self.progress['value'] = 0 + self.choose['state'] = DISABLED + self.fixcover.handle( + action=action, + roots=self.entryvalue.get(), + ) + self.choose['state'] = NORMAL + + def fire_thread(self): threading.Thread( - target=lambda: self.fixcover.handle( - action=action, - roots=self.entryvalue.get() - ) + target=self.handle_ebook_cover, + kwargs={'action': 'fix'}, + daemon=True, ).start() - def create_widgets(self): - self.entry = ttk.Entry(self, width=40, textvariable=self.entryvalue) - self.entry.bind('', lambda e: self.reset_kindle_root()) - self.entry.bind('', lambda e: self.get_kindle_root()) - self.choose = ttk.Button(self, text='Choose', - command=self.get_kindle_root) - self.control = ttk.Frame(self) - self.fix = ttk.Button(self.control, text='Fix Cover', - command=lambda: self.handle_ebook_cover('fix')) - self.clean = ttk.Button(self.control, text='Clean Cover', - command=lambda: self.handle_ebook_cover('clean')) - self.progress = ttk.Progressbar(self, length=580, mode='determinate', - maximum=100) + self.entry = ttk.Entry( + self.control, textvariable=self.entryvalue + ) + self.choose = ttk.Button( + self.control, text='Choose', command=self.get_kindle_root + ) + self.recover = ttk.Button( + self.control, text='Recover', command=self.fire_thread + ) + self.progress = ttk.Progressbar( + self, length=580, mode='determinate', maximum=100 + ) self.log = scrolledtext.ScrolledText(self) - def layout_widgets(self): - self.entry.grid(column=0, row=0, padx=5, sticky=E) - self.choose.grid(column=1, row=0, padx=5, sticky=W) - self.control.grid(column=0, row=1, columnspan=2) - self.fix.grid(column=0, row=1, padx=5, pady=5, sticky=E) - self.clean.grid(column=1, row=1, padx=5, pady=5, sticky=W) - self.progress.grid(column=0, row=2, columnspan=2) - self.log.grid(column=0, row=3, pady=10, columnspan=2) + self.control.grid(column=0, row=0, sticky=W+E) + self.entry.grid(column=0, row=0, sticky=W+E) + self.choose.grid(column=1, row=0, sticky=E) + self.recover.grid(column=2, row=0, sticky=E) + self.progress.grid(column=0, row=1, pady=10) + self.log.grid(column=0, row=2) + + self.control.columnconfigure(0, weight=30) + self.control.columnconfigure(1, weight=1) + self.control.columnconfigure(2, weight=1) + + def bind_action(self): + self.entry.bind('', lambda e: self.reset_kindle_root()) + self.entry.bind('', lambda e: self.get_kindle_root()) def main(): - root = Tk() - root.resizable(width=False, height=False) - - app = Application(root) + app = Application() app.master.withdraw() + if sys.platform.startswith('win32'): + app.master.iconbitmap('assets/icon.ico') app.master.title('Fix Kindle Ebook Cover - %s' % FixCover.version) app.master.update() + app.master.resizable(width=False, height=False) width = app.master.winfo_width() height = app.master.winfo_height() x = round(app.master.winfo_screenwidth() / 2 - width / 2)