kitab-index

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

commit fd683b556f59143e6407c85c7cabee1ea1b2806e
Author: lash <dev@holbrook.no>
Date:   Thu, 27 Aug 2026 16:18:32 -0600

Initial commit

Diffstat:
A.gitignore | 3+++
Akitab.py | 25+++++++++++++++++++++++++
Akitab/__init__.py | 1+
Akitab/index.py | 269+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Arequirements.txt | 3+++
5 files changed, 301 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1,3 @@ +__pycache__ +dist +*egg-info diff --git a/kitab.py b/kitab.py @@ -0,0 +1,25 @@ +import sys +import argparse +import logging + +from kitab import Index + +logging.basicConfig(level=logging.WARNING) +logg = logging.getLogger() + + +def main(): + argp = argparse.ArgumentParser() + argp.add_argument('-d', type=str, default='.', help='default output path') + argp.add_argument('-v', action='store_true', help='turn on verbose logs') + argp.add_argument('path', type=str, help='path to epub to process') + args = argp.parse_args() + if args.v: + logg.setLevel(logging.DEBUG) + o = Index(args.path) + o.load() + o.export_lines(sys.stdout) + + +if __name__ == '__main__': + main() diff --git a/kitab/__init__.py b/kitab/__init__.py @@ -0,0 +1 @@ +from .index import Index diff --git a/kitab/index.py b/kitab/index.py @@ -0,0 +1,269 @@ +import tempfile +import json +import datetime +import logging +import hashlib +import uuid +import io + +import lxml.etree +from epub_utils import Document +import ebooklib +from ebooklib import epub + +logg = logging.getLogger('index') + + +def parse_ns_string(s): + c = s.rfind('}') + if c > 0: + c += 1 + return s[c:].lower() + + +def meta_to_string(meta, fltr=[], prefix=False, single=False): + r = [] + if isinstance(meta, str): + return meta + elif isinstance(meta, list): + for v in meta: + v = meta_to_string(v, fltr=fltr, prefix=prefix) + if v != None: + if single: + return v + r.append(v) + return r + elif isinstance(meta, tuple): + v = None + i = 0 + for k in meta[1].keys(): + i = 1 + if len(fltr) > 0: + kk = parse_ns_string(k) + vv = meta[1][k].lower() + fltrv = '{}={}'.format(kk, vv) + if fltrv in fltr: + pfx = '' + if prefix: + pfx = vv + ':' + v = meta[0] + if len(pfx) == 0 or pfx not in meta[0]: + v = pfx + meta[0] + fltr=[] # skip further checks + else: + v = meta[0] + if i == 0: + v = meta[0] + return v + else: + raise ValueError('cannot process value type: ' + str(type(meta))) + + +def nav_to_jsonl(self): + pass + + +class TreeParser(): + + def __init__(self): + self.items = [] + self.typ = None + self.s = '' + self.i = -1 + + + def start(self, tag, attr): + tag = parse_ns_string(tag) + if tag.lower() == 'p': + self.typ = 'p' + elif tag.lower() == 'h1': + self.typ = 'h1' + else: + logg.info('unhandled tag {}'.format(tag)) + + + def end(self, tag): + if self.typ != None: + self.items.append((self.typ, self.s,)) + self.s = '' + self.typ = None + + + def comment(self, text): + pass + + + def data(self, text): + if len(self.s) > 0: + self.s += "\n" + self.s += text.strip() + + + def close(self): + return 'ok' + + + def __iter__(self): + self.i = 0 + return self + + + def __next__(self): + v = None + try: + v = self.items[self.i] + except IndexError: + self.i = -1 + raise StopIteration() + self.i += 1 + return v + + +class Index(): + + uuns = uuid.UUID('9104f188-2d2f-4d10-903e-7526c7df0df5') + + def __init__(self, path, output_dir=None, uniquename=None): + self.path = path + self.title = None + self.authors = None + self.uu = None + self.z = None + self.ids = None + self.year = None + self.ln = [] + self.uniquename = uniquename + + self._sum() + self.oa = Document(self.path) + self.ob = epub.read_epub(self.path) + + + def _sum(self): + h = hashlib.sha256() + f = open(self.path, 'rb') + v = f.read() + h.update(v) + f.close() + self.z = h.digest() + + h = hashlib.sha256() + f = open(self.path, 'rb') + v = f.read() + h.update(v) + f.close() + self.z = h.digest() + + if self.uniquename == None: + self.uniquename = self.z + if isinstance(self.uniquename, str): + self.uniquename = self.uniquename.encode('utf-8') + self.uu = uuid.uuid5(self.uuns, self.uniquename) + +# +# def _lines_spine(self): +# for v in self.oa.package.spine.itemrefs: +# s = self.oa.find_content_by_id(v['idref']) +# #self.ln.append((0, 0, v['idref'], lxml.etree.tostring(s.to_xml()),)) +# self.ln.append((0, 0, v['idref'], '',)) +# +# +# def _lines_nav(self): +# nav = self.oa.ncx +# if nav == None: +# nav = self.oa.nav +# for v in nav.get_toc_items(): +# try: +# s = self.oa.find_content_by_id(v.id) +# except ValueError as e: +# logg.error('err {}'.format(e)) +# continue +# self.ln.append((v.order, v.level, v.label, s.to_str(),)) +# logg.debug('toc {}'.format(v)) + + + def _lines_nav_alt_content(self, s): + o = TreeParser() + parser = lxml.etree.XMLParser(target=o) + r = lxml.etree.XML(s, parser) + return o + + + def _lines_nav_alt_sub(self, o): + for v in o: + try: + s = self.oa.find_content_by_id(v.id) + except ValueError as e: + logg.error('err {}'.format(e)) + continue + i = 0 + for item in self._lines_nav_alt_content(s.to_str()): + self.ln.append((v.order, v.level, i, v.label, item[0], item[1],)) + i += 1 + logg.debug('child {}'.format(v)) + + + def _lines_nav_alt(self): + nav = self.oa.ncx + if nav == None: + nav = self.oa.nav + for v in nav.get_toc_items(): + if len(v.children) == 0: + continue + self._lines_nav_alt_sub(v.children) + + + def _lines(self): + return self._lines_nav_alt() + + + def load(self): + self.d = tempfile.mkdtemp() + self.title = self.ob.get_metadata('DC', 'title') + self.authors = self.ob.get_metadata('DC', 'creator') + self.ids = self.ob.get_metadata('DC', 'identifier') + self.year = self.ob.get_metadata('DC', 'date') + self._lines() + + + def export_metadata(self): + dt = meta_to_string(self.year, fltr=['event=publication'], single=True) + year = None + try: + year = datetime.datetime.fromisoformat(dt).year + except TypeError: + pass + title = meta_to_string(self.title, single=True) + authors = meta_to_string(self.authors, fltr=['role=aut']) + identifiers = meta_to_string(self.ids, fltr=['scheme=isbn', 'scheme=uuid'], prefix=True) + o = { + 'uuid': str(self.uu), + 'sha256': self.z.hex(), + 'title': title, + 'authors': authors, + 'identifiers': identifiers, + 'year': year, + } + return o + + + def _lines_split_content(self): + lxm + + def export_lines(self, w): + for v in self.ln: + o = { + 'uuid': str(self.uu), + 'order': v[0], + 'level': v[1], + 'seq': v[2], + 'label': v[3], + 'type': v[4], + 'contents': v[5], + } + w.write(json.dumps(o) + '\n') + + + #def __str__(self): + #return json.dumps(self.export()) + #return str(self.export_lines()) diff --git a/requirements.txt b/requirements.txt @@ -0,0 +1,3 @@ +EbookLib==0.20 +epub-utils==0.1.0a1 +lxml==6.1.2