# routes/export_routes.py - Export Routes import io import os import json import base64 import zipfile from flask import Blueprint, request, jsonify, send_file from db import get_db from utils import sanitize_filename, strip_markdown from auth import login_required export_bp = Blueprint('export', __name__) @export_bp.route('/api/export/', methods=['GET']) @login_required def export_project(project_id): """Export project as ZIP file. Only includes chapters with audio.""" db = get_db() cursor = db.cursor() cursor.execute('SELECT * FROM projects WHERE id = ?', (project_id,)) project = cursor.fetchone() if not project: return jsonify({'error': 'Project not found'}), 404 project_name = sanitize_filename(project['name']) cursor.execute(''' SELECT * FROM chapters WHERE project_id = ? ORDER BY chapter_number ''', (project_id,)) chapters = cursor.fetchall() zip_buffer = io.BytesIO() with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zf: manifest = { 'title': project['name'], 'assets': [], 'images': [] } for chapter in chapters: chapter_num = chapter['chapter_number'] cursor.execute(''' SELECT * FROM markdown_blocks WHERE chapter_id = ? ORDER BY block_order ''', (chapter['id'],)) blocks = cursor.fetchall() chapter_has_audio = False for block in blocks: is_image_block = ( (block['content'] and block['content'].strip().startswith('![')) or block['block_type'] == 'image' ) if not is_image_block and block['audio_data']: chapter_has_audio = True break if not chapter_has_audio: continue for block in blocks: block_order = block['block_order'] prefix = f"{chapter_num}.{block_order}" content = block['content'] is_image_block = ( (content and content.strip().startswith('![')) or block['block_type'] == 'image' ) cursor.execute(''' SELECT * FROM block_images WHERE block_id = ? ORDER BY id ''', (block['id'],)) images = cursor.fetchall() image_idx = 0 for img in images: if img['position'] == 'before': image_filename = f"book/{prefix}_img{image_idx}.{img['image_format']}" image_bytes = base64.b64decode(img['image_data']) zf.writestr(image_filename, image_bytes) manifest['images'].append({ 'sortKey': prefix, 'file': image_filename }) image_idx += 1 if is_image_block: for img in images: if img['position'] == 'after': next_prefix = f"{chapter_num}.{block_order + 1}" image_filename = f"book/{next_prefix}_img{image_idx}.{img['image_format']}" image_bytes = base64.b64decode(img['image_data']) zf.writestr(image_filename, image_bytes) manifest['images'].append({ 'sortKey': next_prefix, 'file': image_filename }) image_idx += 1 continue plain_text = strip_markdown(content) if not plain_text.strip(): continue if not block['audio_data']: continue text_filename = f"book/{prefix}_{project_name}.txt" zf.writestr(text_filename, plain_text) asset_entry = { 'prefix': f"{prefix}_", 'sortKey': prefix, 'textFile': text_filename, 'audioFile': None, 'jsonFile': None } audio_filename = f"book/{prefix}_{project_name}.{block['audio_format'] or 'mp3'}" audio_bytes = base64.b64decode(block['audio_data']) zf.writestr(audio_filename, audio_bytes) asset_entry['audioFile'] = audio_filename if block['transcription']: json_filename = f"book/{prefix}_{project_name}.json" zf.writestr(json_filename, block['transcription']) asset_entry['jsonFile'] = json_filename manifest['assets'].append(asset_entry) for img in images: if img['position'] == 'after': next_prefix = f"{chapter_num}.{block_order + 1}" image_filename = f"book/{next_prefix}_img{image_idx}.{img['image_format']}" image_bytes = base64.b64decode(img['image_data']) zf.writestr(image_filename, image_bytes) manifest['images'].append({ 'sortKey': next_prefix, 'file': image_filename }) image_idx += 1 zf.writestr('manifest.json', json.dumps(manifest, indent=2)) reader_templates_dir = os.path.join( os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'reader_templates' ) index_path = os.path.join(reader_templates_dir, 'index.html') if os.path.exists(index_path): with open(index_path, 'r', encoding='utf-8') as f: zf.writestr('index.html', f.read()) reader_path = os.path.join(reader_templates_dir, 'Reader.html') if os.path.exists(reader_path): with open(reader_path, 'r', encoding='utf-8') as f: zf.writestr('Reader.html', f.read()) zip_buffer.seek(0) return send_file( zip_buffer, mimetype='application/zip', as_attachment=True, download_name=f"{project_name}.zip" )