Initial commit: Bookish Flask app for ebook conversion and reading
This commit is contained in:
commit
32245f76ac
20 files changed
+2388
No files matched your search
Whitespace-only changes.
@@ -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},
|
||||
})
|
||||
@@ -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},
|
||||
})
|
||||
Reference in new issue
Block a user