iBook/ipad_bundle_export.py

152 lines
5.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""iPad 数据包导出
生成一个 ZIP包含 iPad 离线展示所需的全部 JSON 与封面缩略图。
结构:
books_meta.json
annotations.json
stats.json
bookintro.json (若存在)
covers/<assetid>.jpg
调用:
from ipad_bundle_export import export_ipad_bundle
export_ipad_bundle(output_zip_path, manager, exporter)
依赖:现有 BookListManager / AnnotationManager / BookNotesExporter / config
"""
from __future__ import annotations
import os, json, zipfile, io, datetime
from typing import Dict, Any
import config
from annotationdata import AnnotationManager
from booklist_parse import BookListManager
from exportbooknotes import BookNotesExporter
try:
from PIL import Image # 可选压缩
_HAS_PIL = True
except Exception:
_HAS_PIL = False
def _ensure_cover_thumbnail(src_path: str, max_side: int = 512) -> bytes:
"""生成封面缩略图二进制数据(JPEG)。失败时返回空字节。"""
if not src_path or not os.path.exists(src_path):
return b''
if not _HAS_PIL:
try:
with open(src_path, 'rb') as f:
return f.read()
except Exception:
return b''
try:
with Image.open(src_path) as im:
im = im.convert('RGB')
w, h = im.size
scale = max_side / float(max(w, h)) if max(w, h) > max_side else 1.0
if scale < 1:
new_size = (int(w*scale), int(h*scale))
im = im.resize(new_size)
bio = io.BytesIO()
im.save(bio, format='JPEG', quality=85)
return bio.getvalue()
except Exception:
return b''
def build_books_meta(manager: BookListManager) -> list[Dict[str, Any]]:
booksinfo = manager.get_books_info()
last_open = manager.get_books_last_open()
finished = manager.get_finished_books_this_year()
finished_set = {fid for fid, *_ in finished}
meta = []
for assetid, info in booksinfo.items():
meta.append({
'id': assetid,
'title': info.get('displayname') or info.get('itemname') or assetid,
'author': info.get('author',''),
'type': info.get('type',''),
'last_open': last_open.get(assetid,{}).get('last_open'),
'readtime30d': info.get('readtime30d', []),
'readtime12m': info.get('readtime12m', []),
'readtime_year': info.get('readtime_year', 0),
'is_finished_this_year': assetid in finished_set,
})
return meta
def build_annotations(bookid: str | None = None) -> Dict[str, Any]:
ann = AnnotationManager(config.LOCAL_ANNOTATION_DB).get_annotations(bookid=bookid)
out = {}
for aid, notes in ann.items():
rows = []
for uuid, rec in notes.items():
rows.append({
'uuid': uuid,
'creationdate': rec.get('creationdate'),
'idref': rec.get('idref'),
'filepos': rec.get('filepos'),
'selected': rec.get('selectedtext'),
'note': rec.get('note')
})
out[aid] = rows
return out
def build_stats(manager: BookListManager) -> Dict[str, Any]:
try:
week = manager.get_total_readtime(days=7)
month = manager.get_total_readtime(days=30)
year12 = manager.get_total_readtime12m()
year_total = manager.get_total_readtime_year()
finished = manager.get_finished_books_this_year()
stats = {
'generated_at': datetime.datetime.utcnow().isoformat()+'Z',
'global': {
'year_total_minutes': year_total,
'month_avg_minutes': int(sum(year12)/12) if year12 else 0,
'week_total_minutes': int(sum(week)),
'day_avg_minutes': int(sum(month)/30) if month else 0,
'finished_books_count': len(finished)
}
}
return stats
except Exception as e:
return {'error': str(e)}
def export_ipad_bundle(output_zip: str, manager: BookListManager | None = None, exporter: BookNotesExporter | None = None) -> str:
"""生成 iPad 数据包 ZIP 并返回路径。"""
manager = manager or BookListManager(plist_path=config.LOCAL_BOOKS_PLIST, db_path=config.LOCAL_LIBRARY_DB)
exporter = exporter or BookNotesExporter(config)
books_meta = build_books_meta(manager)
annotations = build_annotations() # 全量
stats = build_stats(manager)
intro_path = os.path.join(os.path.dirname(__file__), 'bookintro.json')
try:
with open(intro_path,'r',encoding='utf-8') as f:
bookintro = json.load(f)
except Exception:
bookintro = {}
# 准备封面
from cover_mixin import CoverMixin
cm = CoverMixin()
booksinfo = manager.get_books_info()
cover_blobs = {}
for assetid, info in booksinfo.items():
try:
cover_path = cm.find_book_cover(assetid, info) # 需要 config 路径
blob = _ensure_cover_thumbnail(cover_path) if cover_path else b''
if blob:
cover_blobs[assetid] = blob
except Exception:
pass
os.makedirs(os.path.dirname(output_zip) or '.', exist_ok=True)
with zipfile.ZipFile(output_zip, 'w', compression=zipfile.ZIP_DEFLATED) as zf:
zf.writestr('books_meta.json', json.dumps(books_meta, ensure_ascii=False, indent=2))
zf.writestr('annotations.json', json.dumps(annotations, ensure_ascii=False))
zf.writestr('stats.json', json.dumps(stats, ensure_ascii=False, indent=2))
zf.writestr('bookintro.json', json.dumps(bookintro, ensure_ascii=False, indent=2))
for aid, blob in cover_blobs.items():
zf.writestr(f'covers/{aid}.jpg', blob)
return output_zip
if __name__ == '__main__':
out = export_ipad_bundle('ipad_bundle.zip')
print('已生成', out)