This commit is contained in:
douboer
2025-09-07 12:39:28 +08:00
parent 1ba01e3c64
commit 4d033257fe
5714 changed files with 15866 additions and 1032 deletions

151
ipad_bundle_export.py Normal file
View File

@@ -0,0 +1,151 @@
"""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)