Initial commit: Bookish Flask app for ebook conversion and reading

This commit is contained in:
truetype committed 2026-07-22 15:45:07 +02:00
commit 32245f76ac
20 files changed
+2388

No files matched your search

View File
Whitespace-only changes.
+136
View File
@@ -0,0 +1,136 @@
import os
import uuid
import threading
from flask import Blueprint, request, jsonify, Response, send_file, render_template, current_app
from werkzeug.utils import secure_filename
from config import SUPPORTED_FORMATS, OUTPUT_FORMATS_BY_INPUT
from bookish.services.sse_manager import (
create_task, generate_sse_events, update_progress,
mark_error, get_status, cleanup_task
)
from bookish.services.converter import run_conversion, get_format_from_ext
convert_bp = Blueprint("convert", __name__)
ALLOWED_EXTENSIONS = set(SUPPORTED_FORMATS.keys())
def _allowed_file(filename):
ext = os.path.splitext(filename)[1].lower().lstrip(".")
return ext in ALLOWED_EXTENSIONS
def _get_output_ext(output_format):
return f".{output_format}"
def _clean_old_files(folder, hours=1):
import time
now = time.time()
for f in os.listdir(folder):
fpath = os.path.join(folder, f)
if os.path.isfile(fpath) and now - os.path.getmtime(fpath) > hours * 3600:
try:
os.remove(fpath)
except OSError:
pass
@convert_bp.route("/upload", methods=["POST"])
def upload():
if "file" not in request.files:
return jsonify({"error": "Nessun file selezionato"}), 400
file = request.files["file"]
if not file.filename:
return jsonify({"error": "Nessun file selezionato"}), 400
if not _allowed_file(file.filename):
ext = os.path.splitext(file.filename)[1].lower().lstrip(".")
supported = ", ".join(sorted(SUPPORTED_FORMATS.keys()))
return jsonify({"error": f"Formato '{ext}' non supportato. Supportati: {supported}"}), 400
_clean_old_files(current_app.config["UPLOAD_FOLDER"])
_clean_old_files(current_app.config["CONVERTED_FOLDER"])
filename = secure_filename(file.filename)
input_ext = get_format_from_ext(filename)
unique_name = f"{uuid.uuid4().hex}_{filename}"
input_path = os.path.join(current_app.config["UPLOAD_FOLDER"], unique_name)
file.save(input_path)
output_format = request.form.get("output_format", "").lower()
if output_format not in SUPPORTED_FORMATS:
return jsonify({"error": f"Formato output '{output_format}' non supportato"}), 400
available = OUTPUT_FORMATS_BY_INPUT.get(input_ext, [])
if output_format not in available:
return jsonify({
"error": f"Conversione da {input_ext} a {output_format} non supportata"
}), 400
output_ext = _get_output_ext(output_format)
output_name = f"{uuid.uuid4().hex}{output_ext}"
output_path = os.path.join(current_app.config["CONVERTED_FOLDER"], output_name)
task_id = create_task()
thread = threading.Thread(
target=run_conversion,
args=(input_path, output_path, input_ext, output_format, task_id, filename),
daemon=True,
)
thread.start()
return jsonify({
"task_id": task_id,
"input_format": input_ext,
"output_format": output_format,
"filename": filename,
})
@convert_bp.route("/progress/<task_id>")
def progress(task_id):
status = get_status(task_id)
if not status:
return jsonify({"error": "Task not found"}), 404
return Response(
generate_sse_events(task_id),
mimetype="text/event-stream",
headers={
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no",
"Connection": "keep-alive",
},
)
@convert_bp.route("/download/<filename>")
def download(filename):
safe = os.path.basename(filename)
filepath = os.path.join(current_app.config["CONVERTED_FOLDER"], safe)
if not os.path.exists(filepath):
return jsonify({"error": "File non trovato o già rimosso"}), 404
ext = os.path.splitext(safe)[1].lower().lstrip(".")
mime = SUPPORTED_FORMATS.get(ext, {}).get("mime", "application/octet-stream")
download_name = f"converted.{ext}"
return send_file(filepath, mimetype=mime, as_attachment=True, download_name=download_name)
@convert_bp.route("/formats")
def formats():
input_fmt = request.args.get("input", "")
if not input_fmt:
return jsonify({
"formats": {k: v["label"] for k, v in SUPPORTED_FORMATS.items()}
})
available = OUTPUT_FORMATS_BY_INPUT.get(input_fmt, [])
return jsonify({
"input": input_fmt,
"available": available,
"labels": {k: SUPPORTED_FORMATS[k]["label"] for k in available if k in SUPPORTED_FORMATS},
})
+48
View File
@@ -0,0 +1,48 @@
import os
from flask import Blueprint, render_template, jsonify, send_file, current_app
from config import SUPPORTED_FORMATS
reader_bp = Blueprint("reader", __name__)
READABLE_FORMATS = {"epub", "pdf", "html", "md", "txt", "fb2", "docx"}
@reader_bp.route("/reader")
def reader():
return render_template("reader.html")
@reader_bp.route("/reader/<filename>")
def reader_file(filename):
safe = os.path.basename(filename)
filepath = os.path.join(current_app.config["UPLOAD_FOLDER"], safe)
if not os.path.exists(filepath):
filepath = os.path.join(current_app.config["CONVERTED_FOLDER"], safe)
if not os.path.exists(filepath):
return jsonify({"error": "File non trovato"}), 404
ext = os.path.splitext(safe)[1].lower().lstrip(".")
if ext not in READABLE_FORMATS:
return jsonify({"error": f"Formato '{ext}' non supportato dal reader"}), 400
return render_template("reader.html", filename=safe, file_ext=ext)
@reader_bp.route("/reader/serve/<filename>")
def reader_serve(filename):
safe = os.path.basename(filename)
filepath = os.path.join(current_app.config["UPLOAD_FOLDER"], safe)
if not os.path.exists(filepath):
filepath = os.path.join(current_app.config["CONVERTED_FOLDER"], safe)
if not os.path.exists(filepath):
return jsonify({"error": "File non trovato"}), 404
return send_file(filepath, mimetype="application/octet-stream")
@reader_bp.route("/reader/formats")
def reader_formats():
return jsonify({
"formats": list(READABLE_FORMATS),
"labels": {k: SUPPORTED_FORMATS[k]["label"] for k in READABLE_FORMATS if k in SUPPORTED_FORMATS},
})