Skip to content

Commit

Permalink
Added clean orphan ebook covers feature.
Browse files Browse the repository at this point in the history
  • Loading branch information
bookfere committed Jun 17, 2022
1 parent 11e3035 commit c66b772
Show file tree
Hide file tree
Showing 7 changed files with 250 additions and 118 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
__pycache__
*.pyc
.DS_Store
assets
build
262 changes: 169 additions & 93 deletions FixCover.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,47 +8,60 @@
import imghdr
import string
import tempfile
from pathlib import Path

from File import MOBIFile


class FixCover:
version = '1.0'
description = 'A script to fix damaged cover of Kindle ebook.\n\
version = '1.1'
description = 'A tool to fix damaged Kindle ebook covers.\n\
Detail: https://bookfere.com/post/986.html'


def __init__(self, is_cli=False, logger=None, progress=None):
self.is_cli = is_cli
def __init__(self, logger=None, progress=None):
self.logger = logger
self.progress = progress

self.print_log_text('Version: %s' % self.version)
self.print_log_text(self.description, True)
self.print_log('Version: %s' % self.version)
self.print_log(self.description, True)


def print_log_text(self, text, sep=False):
divider = '-------------------------------------------'
text = '%s\n%s\n%s' % (divider, text, divider) \
if sep is True else text
print(text) if self.is_cli else self.logger(text)
def print_log(self, text, sep=False):
if self.logger is not None:
divider = '-------------------------------------------'
text = '%s\n%s\n%s' % (divider, text, divider) \
if sep is True else text
self.logger(text)


def get_filepath_list(self, path):
return glob.glob('%s%s**' % (path, os.sep), recursive=True)
def print_progress(self, factor):
if self.progress is not None:
self.progress(factor)


def get_filename(self, path):
return path.split('/')[-1];
def get_filepath_list(self, path):
return glob.glob('%s%s**' % (path, os.sep), recursive=True)


def get_damaged_thumbnails(self, path):
def get_ebook_thumbnails(self, path):
thumbnails = dict()
for thumbnail in self.get_filepath_list(path):
asin = re.match(rf'.*{re.escape(os.sep)}thumbnail_(.+)_EBOK.+', thumbnail)
if asin is not None and os.path.getsize(thumbnail) < 2000:
asin = re.match(rf'.*{re.escape(os.sep)}thumbnail_(.+)_EBOK.+',
thumbnail)
if asin is not None:
thumbnails[asin.group(1)] = thumbnail
self.print_log_text('- %s' % self.get_filename(thumbnail))
return thumbnails


def get_damaged_thumbnails(self, path):
thumbnails = self.get_ebook_thumbnails(path)
for thumbnail in thumbnails.copy():
thumbnail_path = thumbnails[thumbnail]
if os.path.getsize(thumbnail_path) < 2000:
self.print_log('- %s' % Path(thumbnail_path).name)
else:
del thumbnails[thumbnail]
return thumbnails


Expand All @@ -68,50 +81,136 @@ def get_ebook_list(self, path):
return ebook_list


def fix_damaged_thumbnails(self, path, thumbnails):
ebook_list = self.get_ebook_list(path)
def store_ebook_cover(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

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:
pass

return (ebook_asin, ebook_type, ebook_cover)


def get_thumbnail_name(self, asin):
return 'thumbnail_%s_EBOK_portrait.jpg' % asin


def fix_ebook_thumbnails(self, documents_path, thumbnails_path):
failure_jobs = {
'cover_errors': [],
'ebook_errors': [],
}
for filepath in ebook_list:
if self.progress is not None:
self.progress(len(ebook_list))

try:
mobi_file = MOBIFile(filepath)
except:
continue
self.print_log('Checking damaged ebook covers:', True)
thumbnails = self.get_damaged_thumbnails(thumbnails_path)

ebook_asin = mobi_file.get_metadata('ASIN')
# ebook_type = mobi_file.get_metadata('Document Type')

if ebook_asin in thumbnails.keys():
# self.print_log_text('- %s' % filepath)
store_path = thumbnails[ebook_asin]
filename = self.get_filename(store_path)
try:
cover_data = mobi_file.get_cover_image()
img_format = imghdr.what(None, h=cover_data)
with open(store_path, 'wb') as file:
file.write(cover_data)
self.print_log_text('✓ %s' % filename)
except:
failure_jobs['ebook_errors'].append(
self.get_filename(store_path) +
'\n └ %s' % self.get_filename(filepath)
)
finally:
del thumbnails[ebook_asin]

failure_jobs['cover_errors'] = [
self.get_filename(thumbnail) for thumbnail in thumbnails.values()
]
if len(thumbnails) < 1:
self.print_log('No damaged ebook cover detected.')
return

if self.progress is not None:
self.progress(0)
self.print_log('Fixing damaged ebook covers:', True)
self.print_log('Working...')

ebook_list = self.get_ebook_list(documents_path)
for ebook in ebook_list:
self.print_progress(len(ebook_list))

ebook_asin, ebook_type, ebook_cover = self.get_ebook_metadata(ebook)

ebook = Path(ebook)

if ebook_type == 'EBOK' and ebook_asin in thumbnails.keys():
thumbnail_path = thumbnails[ebook_asin]
thumbnail_name = Path(thumbnail_path).name
if ebook_cover is not None:
self.store_ebook_cover(thumbnail_path, ebook_cover)
self.print_log('✓ Fixed: %s\n └─[%s] %s' % (thumbnail_name,
ebook_type, ebook.name))
else:
failure_jobs['ebook_errors'].append('%s\n └─[%s] %s' %
(ebook_type, thumbnail_name, ebook.name))
del thumbnails[ebook_asin]
elif ebook_type == 'EBOK' and ebook_cover is not None:
thumbnail_name = self.get_thumbnail_name(ebook_asin)
thumbnail_path = os.path.join(thumbnails_path, thumbnail_name)
if not os.path.exists(thumbnail_path):
self.store_ebook_cover(thumbnail_path, ebook_cover)
self.print_log('✓ Generated: %s\n └─[%s] %s' %
(thumbnail_name, ebook_type, ebook.name))
# [BUG] Do this will make Kindle can not open ebook.
# elif ebook_type == 'PDOC' and ebook.suffix == '.azw3' and \
# ebook_cover is not None:
# target = ebook.with_suffix('.mobi')
# ebook.rename(target)
# self.print_log(
# '✓ Rename %s -> %s to show cover.\n └─[%s] %s' %
# (ebook.suffix, target.suffix, ebook_type, target.name)
# )


failure_jobs['cover_errors'] = [Path(thumbnail).name for
thumbnail in thumbnails.values()]

self.print_progress(0)

if failure_jobs is None:
self.print_log('- No ebook cover to fix.')
return

if any(len(job) > 0 for job in failure_jobs.values()):
if len(failure_jobs['cover_errors']) > 0:
self.print_log(
'- These damaged covers have no corresponding ebook.'
)
for job in failure_jobs['cover_errors']:
self.print_log('* %s' % job)


if len(failure_jobs['ebook_errors']) > 0:
self.print_log(
'- The ebooks corresponding to these damaged covers have no'
+ ' covers, you can clean them.'
)
for job in failure_jobs['ebook_errors']:
self.print_log('! %s' % job)
else:
self.print_log('✓ All ebook covers were fixed.')


def clean_orphan_thumbnails(self, documents_path, thumbnails_path):
self.print_log('Analysing orphan ebook covers:', True)
thumbnails = self.get_ebook_thumbnails(thumbnails_path)
ebook_list = self.get_ebook_list(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]

self.print_progress(0)

if len(thumbnails) < 1:
self.print_log('- No orphan covers detected.')
return

return failure_jobs
for thumbnail in thumbnails.values():
thumbnail_path = Path(thumbnail)
thumbnail_path.unlink(True)
self.print_log('✓ Delete: %s' % thumbnail_path.name)


self.print_log('✓ All orphan ebook covers deleted.')


def get_kindle_path(self, path):
Expand All @@ -135,9 +234,9 @@ def get_kindle_root_manually(self, args):
if self.is_kindle_root(path):
roots.append(path)
else:
message = '%s is not a kindle root directory.' % path \
if path != '' else 'You need choose a Kindle root directory.'
self.print_log_text(message)
message = '%s is not a kindle root directory.' % path if \
path != '' else 'You need choose a Kindle root directory first.'
self.print_log(message)
return roots


Expand Down Expand Up @@ -165,9 +264,10 @@ def get_kindle_root(self, roots):
return self.get_kindle_root_automatically()


def handle(self, path = []):
# fix|clean
def handle(self, action='fix', path=[]):
if not sys.version_info >= (3, 5):
self.print_log_text(
self.print_log(
'Rquired Python version >= 3.5\n' +
'You can download here: https://www.python.org/downloads/'
)
Expand All @@ -176,41 +276,17 @@ def handle(self, path = []):
path = [path] if type(path) != list else path
kindle_roots = self.get_kindle_root(path)

if len(kindle_roots) < 1 and self.logger is None:
self.print_log_text('No Kindle root directory detected.')
return

for kindle_root in kindle_roots:
self.print_log_text('Processing Kindle device: %s' % kindle_root)
documents_path, thumbnails_path = self.get_kindle_path(kindle_root)
self.print_log('Processing Kindle device: %s' % kindle_root)

self.print_log_text('Checking damaged ebook covers:', True)
thumbnails = self.get_damaged_thumbnails(thumbnails_path)

if not any(thumbnails):
self.print_log_text('- No ebook covers need to fix.')
return
documents_path, thumbnails_path = self.get_kindle_path(kindle_root)

self.print_log_text('Fixing damaged ebook covers:', True)
self.print_log_text('Working ...')
failure_jobs = self.fix_damaged_thumbnails(documents_path, thumbnails)

if any(len(job) > 0 for job in failure_jobs.values()):
if len(failure_jobs['cover_errors']) > 0:
self.print_log_text(
'- These covers have no corresponding ebook'
)
for job in failure_jobs['cover_errors']:
self.print_log_text('* %s' % job)


if len(failure_jobs['ebook_errors']) > 0:
self.print_log_text(
'- These covers corresponding ebook has no cover'
)
for job in failure_jobs['ebook_errors']:
self.print_log_text('! %s' % job)
if action == 'fix':
self.fix_ebook_thumbnails(documents_path, thumbnails_path)
elif action == 'clean':
self.clean_orphan_thumbnails(documents_path, thumbnails_path)
else:
self.print_log_text('✓ All ebook covers fixed.')
self.print_log('Wrong action.')
return

self.print_log_text('All jobs done.', True)
self.print_log('All jobs done.', True)
22 changes: 16 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,46 +1,56 @@
# Fix-Kindle-Ebook-Cover

This is a Python script to fix damaged ebook cover like below in kindle.
This is a tool to fix damaged ebook cover in kindle as below.

![](screenshots/damaged-kindle-ebook-covers.png)

Detail: [https://bookfere.com/post/986.html](https://bookfere.com/post/986.html)

## Installation

Clone or download this repository:
Install [__Python__](https://www.python.org/downloads/) (Required version __>= 3.5__) and clone or download this repository:

```console
$ git clone https://github.com/bookfere/Fix-Kindle-Ebook-Cover.git
```

You also need to [install Python](https://www.python.org/downloads/) (Required version __>= 3.5__).
You can also use the single file package on [__Releases__](https://github.com/bookfere/Fix-Kindle-Ebook-Cover/releases) page if you do not want to install Python.

* macOS 10.15 (Catalina) or newer.
* Windows 10 (x64) or newer.

## Usage

__GUI version:__

Double click __fix_kindle_ebook_thumbnails.pyw__. and choose a Kindle root directory, then click "Fix Cover" button.
Double click __fix_kindle_ebook_cover_gui.pyw__ (if you are using the single file package, open __Fix Kindle Ebook Cover.app__ or __Fix Kindle Ebook Cover.exe__), choose a Kindle root directory, and click "Fix Cover" button.

![](screenshots/fix-kindle-ebook-cover-gui.png)

__CLI version:__

Run the script __fix_kindle_ebook_thumbnails.py__ on terminal with zero, one or more Kindle root directories.
Run the script __fix_kindle_ebook_cover.py__ on terminal or cmd. It will automatically detect Kindle root directories:

```console
$ python3 fix_kindle_ebook_cover.py
```

You can also specify one or more Kindle root directories:

```console
$ python3 fix_kindle_ebook_cover.py /path/to/kindle
$ python3 fix_kindle_ebook_cover.py /path/to/kindle1 /path/to/kindle2
```

To delete orphan ebook covers, you can add an option `-a clean`.

![](screenshots/fix-kindle-ebook-cover-cli.png)

## Technical details

Most of the heavy lifting is done by other people's code, which is included in this repo:

* Base code is from [Alex Chan](https://github.com/alexwlchan/get-mobi-cover-image). Used under GPL-3.
* Base code is from [get-mobi-cover-image](https://github.com/alexwlchan/get-mobi-cover-image) by __Alex Chan__. Used under GPL-3.
* `File.py` is from [KindleButler](https://github.com/AcidWeb/KindleButler) by **Paweł Jastrzębski**. Used under GPL-3.
* `DualMetaFix.py` by **K. Hendricks**. Used under GPL-3.
* `KindleUnpack.py` by **M. Hannum, P. Durrant, K. Hendricks, S. Siebert, fandrieu, DiapDealer, nickredding**. Used under GPL-3.
Expand Down
Loading

0 comments on commit c66b772

Please sign in to comment.