commit fd683b556f59143e6407c85c7cabee1ea1b2806e
Author: lash <dev@holbrook.no>
Date: Thu, 27 Aug 2026 16:18:32 -0600
Initial commit
Diffstat:
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