Files
audiobook-maker-pro-v4.2/routes/public_routes.py

232 lines
7.9 KiB
Python

# routes/public_routes.py - Public (No Auth) Routes for Published Audiobooks (v4.3)
import re
import os
import json
from flask import Blueprint, jsonify, send_from_directory, send_file, abort
from db import get_db
from media_storage import get_safe_abs_path, read_file_base64
public_bp = Blueprint('public', __name__)
# ============================================
# Helpers
# ============================================
_CONTROL_CHAR_RE = re.compile(r'[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]')
def clean_str(s):
if s is None:
return ''
if not isinstance(s, str):
s = str(s)
return _CONTROL_CHAR_RE.sub('', s)
def clean_transcription(transcription):
if isinstance(transcription, list):
for t in transcription:
if isinstance(t, dict) and 'word' in t:
t['word'] = clean_str(t.get('word', ''))
return transcription
# ============================================
# Routes
# ============================================
@public_bp.route('/home')
def public_home():
"""Public homepage - Bookcase view of published audiobooks."""
return send_from_directory('templates', 'public_home.html')
@public_bp.route('/read/<int:project_id>')
def public_reader(project_id):
"""Public reader page for a published audiobook."""
db = get_db()
cursor = db.cursor()
cursor.execute('SELECT id, is_published FROM projects WHERE id = ?', (project_id,))
project = cursor.fetchone()
if not project or not project['is_published']:
abort(404)
cursor.execute('UPDATE projects SET view_count = view_count + 1 WHERE id = ?', (project_id,))
db.commit()
return send_from_directory('templates', 'public_reader.html')
@public_bp.route('/api/public/books', methods=['GET'])
def list_published_books():
"""List all published audiobooks (no auth required)."""
db = get_db()
cursor = db.cursor()
cursor.execute('''
SELECT p.id, p.name, p.description, p.author, p.category,
p.thumbnail_data, p.thumbnail_format, p.thumbnail_path, p.published_at,
p.view_count, p.created_at,
(SELECT COUNT(*) FROM chapters WHERE project_id = p.id) as chapter_count
FROM projects p
WHERE p.is_published = 1
ORDER BY p.published_at DESC
''')
books = []
for row in cursor.fetchall():
thumb_data = row['thumbnail_data']
if row['thumbnail_path']:
thumb_data = read_file_base64(row['thumbnail_path'])
books.append({
'id': row['id'],
'name': row['name'],
'description': row['description'] or '',
'author': row['author'] or '',
'category': row['category'] or '',
'thumbnail_data': thumb_data,
'thumbnail_format': row['thumbnail_format'] or 'png',
'published_at': row['published_at'],
'view_count': row['view_count'] or 0,
'chapter_count': row['chapter_count']
})
return jsonify({'books': books})
@public_bp.route('/api/public/books/<int:project_id>', methods=['GET'])
def get_published_book(project_id):
"""Get book metadata WITHOUT audio_data (lazy-loaded separately)."""
db = get_db()
cursor = db.cursor()
cursor.execute('''
SELECT * FROM projects WHERE id = ? AND is_published = 1
''', (project_id,))
project = cursor.fetchone()
if not project:
return jsonify({'error': 'Book not found or not published'}), 404
cursor.execute('''
SELECT * FROM chapters WHERE project_id = ? ORDER BY chapter_number
''', (project_id,))
chapters = cursor.fetchall()
chapters_data = []
for chapter in chapters:
cursor.execute('''
SELECT id, block_order, block_type, content, audio_format, audio_path, transcription,
((audio_data IS NOT NULL AND audio_data != '')
OR (audio_path IS NOT NULL AND audio_path != '')) as has_audio
FROM markdown_blocks WHERE chapter_id = ? ORDER BY block_order
''', (chapter['id'],))
blocks = cursor.fetchall()
blocks_data = []
for block in blocks:
cursor.execute('''
SELECT image_data, image_format, alt_text, position, image_path
FROM block_images WHERE block_id = ? ORDER BY id
''', (block['id'],))
images = cursor.fetchall()
transcription = []
if block['transcription']:
try:
transcription = json.loads(block['transcription'])
transcription = clean_transcription(transcription)
except (json.JSONDecodeError, TypeError):
transcription = []
images_data = []
for img in images:
img_data = ''
if img['image_path']:
img_data = read_file_base64(img['image_path'])
elif img['image_data']:
img_data = clean_str(img['image_data'])
images_data.append({
'data': img_data,
'format': clean_str(img['image_format']) or 'png',
'alt_text': clean_str(img['alt_text']),
'position': clean_str(img['position']) or 'before'
})
blocks_data.append({
'id': block['id'],
'block_order': block['block_order'],
'block_type': clean_str(block['block_type']),
'content': clean_str(block['content']),
'audio_data': '',
'audio_format': clean_str(block['audio_format']) or 'mp3',
'has_audio': bool(block['has_audio']),
'transcription': transcription,
'images': images_data
})
chapters_data.append({
'id': chapter['id'],
'chapter_number': chapter['chapter_number'],
'title': clean_str(chapter['title']),
'blocks': blocks_data
})
thumb_data = project['thumbnail_data']
if project['thumbnail_path']:
thumb_data = read_file_base64(project['thumbnail_path'])
return jsonify({
'id': project['id'],
'name': clean_str(project['name']),
'description': clean_str(project['description']) if project['description'] else '',
'author': clean_str(project['author']) if project['author'] else '',
'thumbnail_data': thumb_data,
'thumbnail_format': project['thumbnail_format'] or 'png',
'chapters': chapters_data
})
@public_bp.route('/api/public/books/<int:project_id>/audio/<int:block_id>', methods=['GET'])
def get_public_block_audio(project_id, block_id):
"""Return audio as base64 JSON for a published book block (v4.3)."""
db = get_db()
cursor = db.cursor()
cursor.execute('SELECT is_published FROM projects WHERE id = ?', (project_id,))
project = cursor.fetchone()
if not project or not project['is_published']:
return jsonify({'error': 'Book not found or not published'}), 404
cursor.execute('''
SELECT mb.audio_data, mb.audio_path, mb.audio_format
FROM markdown_blocks mb
JOIN chapters c ON mb.chapter_id = c.id
WHERE mb.id = ? AND c.project_id = ?
''', (block_id, project_id))
row = cursor.fetchone()
if not row:
return jsonify({'error': 'Block not found'}), 404
audio_format = clean_str(row['audio_format']) or 'mp3'
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})
if row['audio_data']:
return jsonify({
'audio_data': clean_str(row['audio_data']),
'audio_format': audio_format
})
return jsonify({'audio_data': '', 'audio_format': audio_format})