v4.3 ui: 3D wooden bookshelf redesign, fix player cover aspect ratio, and smooth subtitle scroll
This commit is contained in:
@@ -11,8 +11,10 @@ from auth import login_required
|
||||
from media_storage import (
|
||||
save_audio, save_image, save_thumbnail,
|
||||
read_file_base64, get_safe_abs_path,
|
||||
delete_project_media, get_storage_usage_bytes
|
||||
delete_project_media, get_storage_usage_bytes,
|
||||
read_pending_thumbnail, delete_pending_thumbnail # v4.4
|
||||
)
|
||||
from thumbnail_generator import generate_text_cover # v4.4 backfill
|
||||
|
||||
project_bp = Blueprint('project', __name__)
|
||||
|
||||
@@ -259,7 +261,7 @@ def get_block_audio(project_id, block_id):
|
||||
@project_bp.route('/api/projects/<int:project_id>', methods=['PUT'])
|
||||
@login_required
|
||||
def update_project(project_id):
|
||||
"""Update project name."""
|
||||
"""Update project name and metadata (author/description/category)."""
|
||||
data = request.json
|
||||
name = data.get('name', '').strip()
|
||||
|
||||
@@ -269,10 +271,28 @@ def update_project(project_id):
|
||||
db = get_db()
|
||||
cursor = db.cursor()
|
||||
|
||||
# মেটাডেটা ফিল্ড — পাঠানো হলেই আপডেট হবে
|
||||
updates = ['name = ?']
|
||||
params = [name]
|
||||
|
||||
if 'author' in data:
|
||||
updates.append('author = ?')
|
||||
params.append((data.get('author') or '').strip())
|
||||
if 'description' in data:
|
||||
updates.append('description = ?')
|
||||
params.append((data.get('description') or '').strip())
|
||||
if 'category' in data:
|
||||
updates.append('category = ?')
|
||||
params.append((data.get('category') or '').strip())
|
||||
|
||||
updates.append('updated_at = CURRENT_TIMESTAMP')
|
||||
params.append(project_id)
|
||||
|
||||
try:
|
||||
cursor.execute('''
|
||||
UPDATE projects SET name = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?
|
||||
''', (name, project_id))
|
||||
cursor.execute(
|
||||
f"UPDATE projects SET {', '.join(updates)} WHERE id = ?",
|
||||
params
|
||||
)
|
||||
db.commit()
|
||||
|
||||
if cursor.rowcount == 0:
|
||||
@@ -422,6 +442,32 @@ def save_project_content(project_id):
|
||||
(img_rel, image_id)
|
||||
)
|
||||
|
||||
# --- v4.4: pending auto-thumbnail commit করা (শুধু যদি প্রজেক্টে থাম্বনেইল না থাকে) ---
|
||||
pending_token = clean_str(data.get('pending_thumbnail', ''))
|
||||
if pending_token:
|
||||
cursor.execute(
|
||||
'SELECT thumbnail_path, thumbnail_data FROM projects WHERE id = ?',
|
||||
(project_id,)
|
||||
)
|
||||
prow = cursor.fetchone()
|
||||
already_has_thumb = prow and (prow['thumbnail_path'] or prow['thumbnail_data'])
|
||||
|
||||
if not already_has_thumb:
|
||||
thumb_bytes, thumb_fmt = read_pending_thumbnail(pending_token)
|
||||
if thumb_bytes:
|
||||
rel_path = save_thumbnail(project_id, thumb_bytes, thumb_fmt)
|
||||
if rel_path:
|
||||
cursor.execute('''
|
||||
UPDATE projects
|
||||
SET thumbnail_path = ?, thumbnail_data = NULL, thumbnail_format = ?,
|
||||
thumbnail_auto = 1
|
||||
WHERE id = ?
|
||||
''', (rel_path, thumb_fmt, project_id))
|
||||
print(f" 🖼️ Auto-thumbnail applied to project {project_id}")
|
||||
|
||||
# commit বা বাতিল — যেভাবেই হোক pending ফাইল মুছে ফেলি
|
||||
delete_pending_thumbnail(pending_token)
|
||||
|
||||
cursor.execute('''
|
||||
UPDATE projects SET updated_at = CURRENT_TIMESTAMP WHERE id = ?
|
||||
''', (project_id,))
|
||||
@@ -531,7 +577,8 @@ def upload_thumbnail(project_id):
|
||||
rel_path = save_thumbnail(project_id, img_bytes, fmt)
|
||||
|
||||
cursor.execute('''
|
||||
UPDATE projects SET thumbnail_path = ?, thumbnail_data = NULL, thumbnail_format = ?
|
||||
UPDATE projects SET thumbnail_path = ?, thumbnail_data = NULL, thumbnail_format = ?,
|
||||
thumbnail_auto = 0
|
||||
WHERE id = ?
|
||||
''', (rel_path, fmt, project_id))
|
||||
db.commit()
|
||||
@@ -565,12 +612,230 @@ def delete_thumbnail(project_id):
|
||||
# v4.3: Database Maintenance (VACUUM + stats)
|
||||
# ============================================
|
||||
|
||||
@project_bp.route('/api/projects/<int:project_id>/generate-thumbnail', methods=['POST'])
|
||||
@login_required
|
||||
def generate_single_thumbnail(project_id):
|
||||
"""
|
||||
একটি নির্দিষ্ট প্রজেক্টের থাম্বনেইল auto-generate/regenerate করে (v4.4)।
|
||||
সোর্স: (১) প্রথম embedded image block, (২) fallback হিসেবে text-cover।
|
||||
"""
|
||||
import base64
|
||||
from thumbnail_generator import _optimize_image_bytes
|
||||
|
||||
db = get_db()
|
||||
cursor = db.cursor()
|
||||
|
||||
cursor.execute('SELECT id, name, author FROM projects WHERE id = ?', (project_id,))
|
||||
proj = cursor.fetchone()
|
||||
if not proj:
|
||||
return jsonify({'error': 'Project not found'}), 404
|
||||
|
||||
thumb_bytes = None
|
||||
thumb_fmt = None
|
||||
source = None
|
||||
|
||||
# সোর্স ১: প্রথম embedded image
|
||||
cursor.execute('''
|
||||
SELECT bi.image_data, bi.image_path, bi.image_format
|
||||
FROM block_images bi
|
||||
JOIN markdown_blocks mb ON bi.block_id = mb.id
|
||||
JOIN chapters c ON mb.chapter_id = c.id
|
||||
WHERE c.project_id = ?
|
||||
AND ((bi.image_path IS NOT NULL AND bi.image_path != '')
|
||||
OR (bi.image_data IS NOT NULL AND bi.image_data != ''))
|
||||
ORDER BY c.chapter_number, mb.block_order, bi.id
|
||||
LIMIT 1
|
||||
''', (project_id,))
|
||||
img_row = cursor.fetchone()
|
||||
|
||||
if img_row:
|
||||
raw = None
|
||||
if img_row['image_path']:
|
||||
b64 = read_file_base64(img_row['image_path'])
|
||||
if b64:
|
||||
try:
|
||||
raw = base64.b64decode(b64)
|
||||
except Exception:
|
||||
raw = None
|
||||
elif img_row['image_data']:
|
||||
try:
|
||||
raw = base64.b64decode(clean_str(img_row['image_data']))
|
||||
except Exception:
|
||||
raw = None
|
||||
|
||||
if raw and len(raw) > 4000:
|
||||
thumb_bytes, thumb_fmt = _optimize_image_bytes(
|
||||
raw, img_row['image_format'] or 'png'
|
||||
)
|
||||
if thumb_bytes:
|
||||
source = 'image'
|
||||
|
||||
# সোর্স ২: text-cover fallback
|
||||
if not thumb_bytes:
|
||||
thumb_bytes, thumb_fmt = generate_text_cover(proj['name'], proj['author'] or '')
|
||||
if thumb_bytes:
|
||||
source = 'text'
|
||||
|
||||
if not thumb_bytes:
|
||||
return jsonify({'error': 'Could not generate thumbnail (Pillow may be missing)'}), 500
|
||||
|
||||
rel_path = save_thumbnail(project_id, thumb_bytes, thumb_fmt)
|
||||
if not rel_path:
|
||||
return jsonify({'error': 'Failed to save thumbnail'}), 500
|
||||
|
||||
cursor.execute('''
|
||||
UPDATE projects
|
||||
SET thumbnail_path = ?, thumbnail_data = NULL, thumbnail_format = ?,
|
||||
thumbnail_auto = 1
|
||||
WHERE id = ?
|
||||
''', (rel_path, thumb_fmt, project_id))
|
||||
db.commit()
|
||||
|
||||
b64 = read_file_base64(rel_path)
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'source': source,
|
||||
'thumbnail_data': b64,
|
||||
'thumbnail_format': thumb_fmt,
|
||||
'message': f'Thumbnail generated from {"first image" if source == "image" else "text cover"}'
|
||||
})
|
||||
|
||||
|
||||
@project_bp.route('/api/maintenance/backfill-thumbnails', methods=['POST'])
|
||||
@login_required
|
||||
def backfill_thumbnails():
|
||||
"""
|
||||
থাম্বনেইল জেনারেট করে (v4.4)।
|
||||
সোর্স: (১) প্রথম embedded image block, (২) fallback হিসেবে text-cover।
|
||||
|
||||
force=False → শুধু থাম্বনেইল-বিহীন প্রজেক্ট।
|
||||
force=True → auto-generated থাম্বনেইলও নতুন করে বানায় (user-uploaded রক্ষা পায়)।
|
||||
"""
|
||||
data = request.json or {}
|
||||
force = bool(data.get('force', False))
|
||||
|
||||
db = get_db()
|
||||
cursor = db.cursor()
|
||||
|
||||
if force:
|
||||
# user-uploaded (thumbnail_auto=0 AND থাম্বনেইল আছে) ছাড়া সব
|
||||
cursor.execute('''
|
||||
SELECT id, name, author FROM projects
|
||||
WHERE (thumbnail_path IS NULL OR thumbnail_path = '')
|
||||
OR thumbnail_auto = 1
|
||||
''')
|
||||
else:
|
||||
cursor.execute('''
|
||||
SELECT id, name, author FROM projects
|
||||
WHERE (thumbnail_path IS NULL OR thumbnail_path = '')
|
||||
AND (thumbnail_data IS NULL OR thumbnail_data = '')
|
||||
''')
|
||||
projects = cursor.fetchall()
|
||||
|
||||
generated = 0
|
||||
from_image = 0
|
||||
from_text = 0
|
||||
failed = 0
|
||||
|
||||
for proj in projects:
|
||||
pid = proj['id']
|
||||
thumb_bytes = None
|
||||
thumb_fmt = None
|
||||
|
||||
# সোর্স ১: প্রথম embedded image (path বা base64)
|
||||
cursor.execute('''
|
||||
SELECT bi.image_data, bi.image_path, bi.image_format
|
||||
FROM block_images bi
|
||||
JOIN markdown_blocks mb ON bi.block_id = mb.id
|
||||
JOIN chapters c ON mb.chapter_id = c.id
|
||||
WHERE c.project_id = ?
|
||||
AND ((bi.image_path IS NOT NULL AND bi.image_path != '')
|
||||
OR (bi.image_data IS NOT NULL AND bi.image_data != ''))
|
||||
ORDER BY c.chapter_number, mb.block_order, bi.id
|
||||
LIMIT 1
|
||||
''', (pid,))
|
||||
img_row = cursor.fetchone()
|
||||
|
||||
if img_row:
|
||||
import base64
|
||||
raw = None
|
||||
if img_row['image_path']:
|
||||
raw = read_file_base64(img_row['image_path'])
|
||||
if raw:
|
||||
try:
|
||||
raw = base64.b64decode(raw)
|
||||
except Exception:
|
||||
raw = None
|
||||
elif img_row['image_data']:
|
||||
try:
|
||||
raw = base64.b64decode(clean_str(img_row['image_data']))
|
||||
except Exception:
|
||||
raw = None
|
||||
|
||||
if raw and len(raw) > 4000:
|
||||
from thumbnail_generator import _optimize_image_bytes
|
||||
thumb_bytes, thumb_fmt = _optimize_image_bytes(
|
||||
raw, img_row['image_format'] or 'png'
|
||||
)
|
||||
if thumb_bytes:
|
||||
from_image += 1
|
||||
|
||||
# সোর্স ২: text-cover fallback
|
||||
if not thumb_bytes:
|
||||
thumb_bytes, thumb_fmt = generate_text_cover(
|
||||
proj['name'], proj['author'] or ''
|
||||
)
|
||||
if thumb_bytes:
|
||||
from_text += 1
|
||||
|
||||
if not thumb_bytes:
|
||||
failed += 1
|
||||
continue
|
||||
|
||||
rel_path = save_thumbnail(pid, thumb_bytes, thumb_fmt)
|
||||
if rel_path:
|
||||
cursor.execute('''
|
||||
UPDATE projects
|
||||
SET thumbnail_path = ?, thumbnail_data = NULL, thumbnail_format = ?,
|
||||
thumbnail_auto = 1
|
||||
WHERE id = ?
|
||||
''', (rel_path, thumb_fmt, pid))
|
||||
generated += 1
|
||||
else:
|
||||
failed += 1
|
||||
|
||||
db.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'total_without_thumbnail': len(projects),
|
||||
'generated': generated,
|
||||
'from_image': from_image,
|
||||
'from_text': from_text,
|
||||
'failed': failed,
|
||||
'force': force,
|
||||
'message': f'{generated} thumbnails generated '
|
||||
f'({from_image} from images, {from_text} text covers).'
|
||||
})
|
||||
|
||||
|
||||
@project_bp.route('/api/maintenance/db-stats', methods=['GET'])
|
||||
@login_required
|
||||
def db_stats():
|
||||
"""ডেটাবেস সাইজ, ফাঁকা স্পেস (%), এবং মিডিয়া স্টোরেজ সাইজ রিটার্ন করে।"""
|
||||
stats = get_db_stats()
|
||||
media_bytes = get_storage_usage_bytes()
|
||||
try:
|
||||
stats = get_db_stats()
|
||||
except Exception as e:
|
||||
print(f"⚠️ get_db_stats failed: {e}")
|
||||
return jsonify({'error': f'Database stats failed: {str(e)}'}), 500
|
||||
|
||||
# মিডিয়া স্ক্যান আলাদা try/except — ফোল্ডার বিশাল/দুর্গম হলেও stats রিটার্ন হবে
|
||||
try:
|
||||
media_bytes = get_storage_usage_bytes()
|
||||
except Exception as e:
|
||||
print(f"⚠️ get_storage_usage_bytes failed: {e}")
|
||||
media_bytes = 0
|
||||
|
||||
stats['media_size_bytes'] = media_bytes
|
||||
stats['media_size_mb'] = round(media_bytes / (1024 * 1024), 2)
|
||||
return jsonify(stats)
|
||||
|
||||
Reference in New Issue
Block a user