From 0a2f4574764189c96769fcfba76d80fd52a5ae32 Mon Sep 17 00:00:00 2001 From: Ashim Kumar Date: Fri, 12 Jun 2026 18:40:12 +0600 Subject: [PATCH] v4.3 fix: serve audio as base64 JSON (gunicorn Range fix), path-aware export, faster project load --- routes/export_routes.py | 99 ++++++++++++++++++++++++++-------------- routes/project_routes.py | 22 +++++---- routes/public_routes.py | 16 ++++--- 3 files changed, 87 insertions(+), 50 deletions(-) diff --git a/routes/export_routes.py b/routes/export_routes.py index 171e8d4..f5aa140 100644 --- a/routes/export_routes.py +++ b/routes/export_routes.py @@ -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/', 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" - ) \ No newline at end of file + ) diff --git a/routes/project_routes.py b/routes/project_routes.py index eca6567..d5e38d0 100644 --- a/routes/project_routes.py +++ b/routes/project_routes.py @@ -222,7 +222,7 @@ def get_project(project_id): @project_bp.route('/api/projects//audio/', methods=['GET']) @login_required def get_block_audio(project_id, block_id): - """Stream audio for a single block (v4.3: from file, with base64 fallback).""" + """Return audio as base64 JSON (v4.3: read from file, no send_file/Range).""" db = get_db() cursor = db.cursor() @@ -237,21 +237,23 @@ def get_block_audio(project_id, block_id): if not row: return jsonify({'error': 'Block not found'}), 404 - # নতুন: ফাইল থেকে সরাসরি stream (Range request সাপোর্ট সহ) - if row['audio_path']: - abs_path = get_safe_abs_path(row['audio_path']) - if abs_path and os.path.exists(abs_path): - return send_file(abs_path, mimetype=f"audio/{row['audio_format'] or 'mp3'}", - conditional=True) + audio_format = clean_str(row['audio_format']) or 'mp3' - # পুরোনো: base64 JSON + # নতুন: ফাইল থেকে পড়ে base64 হিসেবে পাঠাই (gunicorn Range সমস্যা এড়াতে) + if row['audio_path']: + from media_storage import read_file_base64 + b64 = read_file_base64(row['audio_path']) + if b64: + return jsonify({'audio_data': b64, 'audio_format': audio_format}) + + # পুরোনো: DB-তে থাকা base64 (মাইগ্রেট না হওয়া) if row['audio_data']: return jsonify({ 'audio_data': clean_str(row['audio_data']), - 'audio_format': clean_str(row['audio_format']) or 'mp3' + 'audio_format': audio_format }) - return jsonify({'audio_data': '', 'audio_format': row['audio_format'] or 'mp3'}) + return jsonify({'audio_data': '', 'audio_format': audio_format}) @project_bp.route('/api/projects/', methods=['PUT']) diff --git a/routes/public_routes.py b/routes/public_routes.py index 2d96826..1167ef6 100644 --- a/routes/public_routes.py +++ b/routes/public_routes.py @@ -194,7 +194,7 @@ def get_published_book(project_id): @public_bp.route('/api/public/books//audio/', methods=['GET']) def get_public_block_audio(project_id, block_id): - """Stream audio file for a published book block (v4.3).""" + """Return audio as base64 JSON for a published book block (v4.3).""" db = get_db() cursor = db.cursor() @@ -214,16 +214,18 @@ def get_public_block_audio(project_id, block_id): if not row: return jsonify({'error': 'Block not found'}), 404 + audio_format = clean_str(row['audio_format']) or 'mp3' + if row['audio_path']: - abs_path = get_safe_abs_path(row['audio_path']) - if abs_path and os.path.exists(abs_path): - return send_file(abs_path, mimetype=f"audio/{row['audio_format'] or 'mp3'}", - conditional=True) + from media_storage import read_file_base64 + b64 = read_file_base64(row['audio_path']) + if b64: + return jsonify({'audio_data': b64, 'audio_format': audio_format}) if row['audio_data']: return jsonify({ 'audio_data': clean_str(row['audio_data']), - 'audio_format': clean_str(row['audio_format']) or 'mp3' + 'audio_format': audio_format }) - return jsonify({'audio_data': '', 'audio_format': row['audio_format'] or 'mp3'}) + return jsonify({'audio_data': '', 'audio_format': audio_format})