v4.3 fix: serve audio as base64 JSON (gunicorn Range fix), path-aware export, faster project load

This commit is contained in:
Ashim Kumar
2026-06-12 18:40:12 +06:00
parent cc57204aff
commit 0a2f457476
3 changed files with 87 additions and 50 deletions

View File

@@ -1,4 +1,4 @@
# routes/export_routes.py - Export Routes
# routes/export_routes.py - Export Routes (v4.3: file-based media)
import io
import os
@@ -11,9 +11,39 @@ 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):
@@ -52,13 +82,15 @@ def export_project(project_id):
''', (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'
)
if not is_image_block and block['audio_data']:
has_audio = bool(block['audio_path']) or bool(block['audio_data'])
if not is_image_block and has_audio:
chapter_has_audio = True
break
@@ -83,34 +115,37 @@ def export_project(project_id):
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
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':
next_prefix = f"{section_id}.{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
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
if not block['audio_data']:
audio_bytes = _resolve_audio_bytes(block)
if not audio_bytes:
continue
text_filename = f"book/{prefix}_{project_name}.txt"
@@ -126,7 +161,6 @@ def export_project(project_id):
}
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
@@ -139,15 +173,16 @@ def export_project(project_id):
for img in images:
if img['position'] == 'after':
next_prefix = f"{section_id}.{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
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)
@@ -163,7 +198,6 @@ def export_project(project_id):
if os.path.exists(index_path):
with open(index_path, 'r', encoding='utf-8') as f:
html_content = f.read()
# Inject manifest into index.html safely
html_content = html_content.replace('/*{{MANIFEST_DATA}}*/ null', manifest_json_str)
zf.writestr('index.html', html_content)
@@ -171,7 +205,6 @@ def export_project(project_id):
if os.path.exists(reader_path):
with open(reader_path, 'r', encoding='utf-8') as f:
html_content = f.read()
# Inject manifest into Reader.html safely
html_content = html_content.replace('/*{{MANIFEST_DATA}}*/ null', manifest_json_str)
zf.writestr('Reader.html', html_content)
@@ -182,4 +215,4 @@ def export_project(project_id):
mimetype='application/zip',
as_attachment=True,
download_name=f"{project_name}.zip"
)
)