# routes/export_routes.py - Export Routes (v4.3: file-based media) import io import os import json import base64 import zipfile import re 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 from media_storage import read_file_bytes export_bp = Blueprint('export', __name__) def _resolve_audio_bytes(block): """audio_path থাকলে ফাইল থেকে, নইলে DB base64 থেকে bytes রিটার্ন করে।""" if block['audio_path']: data = read_file_bytes(block['audio_path']) if data: return data if block['audio_data']: try: return base64.b64decode(block['audio_data']) except Exception: return None return None def _resolve_image_bytes(img): """image_path থাকলে ফাইল থেকে, নইলে DB base64 থেকে bytes রিটার্ন করে।""" if img['image_path']: data = read_file_bytes(img['image_path']) if data: return data if img['image_data']: try: return base64.b64decode(img['image_data']) except Exception: return None return None @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: section_id = chapter['chapter_number'] section_title = chapter['title'] cursor.execute(''' SELECT * FROM markdown_blocks WHERE chapter_id = ? ORDER BY block_order ''', (chapter['id'],)) blocks = cursor.fetchall() # চ্যাপ্টারে কোনো অডিও আছে কিনা (file বা base64) chapter_has_audio = False for block in blocks: is_image_block = ( (block['content'] and block['content'].strip().startswith('![')) or block['block_type'] == 'image' ) has_audio = bool(block['audio_path']) or bool(block['audio_data']) if not is_image_block and has_audio: chapter_has_audio = True break if not chapter_has_audio: continue for block in blocks: block_order = block['block_order'] prefix = f"{section_id}.{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': img_bytes = _resolve_image_bytes(img) if img_bytes: image_filename = f"book/{prefix}_img{image_idx}.{img['image_format']}" zf.writestr(image_filename, img_bytes) manifest['images'].append({ 'sortKey': prefix, 'file': image_filename }) image_idx += 1 if is_image_block: for img in images: if img['position'] == 'after': img_bytes = _resolve_image_bytes(img) if img_bytes: next_prefix = f"{section_id}.{block_order + 1}" image_filename = f"book/{next_prefix}_img{image_idx}.{img['image_format']}" zf.writestr(image_filename, img_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 audio_bytes = _resolve_audio_bytes(block) if not audio_bytes: continue text_filename = f"book/{prefix}_{project_name}.txt" zf.writestr(text_filename, plain_text) asset_entry = { 'prefix': f"{prefix}_", 'sortKey': prefix, 'sectionName': section_title, 'textFile': text_filename, 'audioFile': None, 'jsonFile': None } audio_filename = f"book/{prefix}_{project_name}.{block['audio_format'] or 'mp3'}" 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': img_bytes = _resolve_image_bytes(img) if img_bytes: next_prefix = f"{section_id}.{block_order + 1}" image_filename = f"book/{next_prefix}_img{image_idx}.{img['image_format']}" zf.writestr(image_filename, img_bytes) manifest['images'].append({ 'sortKey': next_prefix, 'file': image_filename }) image_idx += 1 # Write manifest.json to zip root manifest_json_str = json.dumps(manifest, indent=2) zf.writestr('manifest.json', manifest_json_str) # --- DYNAMIC INJECTION FOR Reader.html & index.html --- 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: html_content = f.read() html_content = html_content.replace('/*{{MANIFEST_DATA}}*/ null', manifest_json_str) zf.writestr('index.html', html_content) 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: html_content = f.read() html_content = html_content.replace('/*{{MANIFEST_DATA}}*/ null', manifest_json_str) zf.writestr('Reader.html', html_content) zip_buffer.seek(0) return send_file( zip_buffer, mimetype='application/zip', as_attachment=True, download_name=f"{project_name}.zip" )