219 lines
8.4 KiB
Python
219 lines
8.4 KiB
Python
# 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/<int:project_id>', 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"
|
|
)
|