Release 1.7
This commit is contained in:
@@ -1114,6 +1114,149 @@ def add_playlist_item(playlist_id: int):
|
||||
return redirect(url_for("company.playlist_detail", playlist_id=playlist_id))
|
||||
|
||||
|
||||
@bp.post("/playlists/<int:playlist_id>/items/bulk-images")
|
||||
@login_required
|
||||
def bulk_upload_playlist_images(playlist_id: int):
|
||||
"""Bulk upload multiple images to a playlist.
|
||||
|
||||
Expects multipart/form-data:
|
||||
- files: multiple image files
|
||||
- crop_mode: "16:9" or "9:16" (optional; defaults to 16:9)
|
||||
- duration_seconds: optional (defaults to 10)
|
||||
|
||||
Returns JSON:
|
||||
{ ok: true, items: [...] }
|
||||
"""
|
||||
|
||||
company_user_required()
|
||||
|
||||
playlist = db.session.get(Playlist, playlist_id)
|
||||
if not playlist or playlist.company_id != current_user.company_id:
|
||||
abort(404)
|
||||
|
||||
# This endpoint is intended for AJAX.
|
||||
def _json_error(message: str, status: int = 400):
|
||||
return jsonify({"ok": False, "error": message}), status
|
||||
|
||||
files = request.files.getlist("files")
|
||||
if not files:
|
||||
return _json_error("No files uploaded")
|
||||
|
||||
crop_mode = (request.form.get("crop_mode") or "16:9").strip().lower()
|
||||
if crop_mode not in {"16:9", "9:16"}:
|
||||
crop_mode = "16:9"
|
||||
|
||||
raw_duration = request.form.get("duration_seconds")
|
||||
try:
|
||||
duration = int(raw_duration) if raw_duration is not None else 10
|
||||
except (TypeError, ValueError):
|
||||
duration = 10
|
||||
duration = max(1, duration)
|
||||
|
||||
# Quota check before processing.
|
||||
with db.session.no_autoflush:
|
||||
company = db.session.get(Company, current_user.company_id)
|
||||
if not company:
|
||||
abort(404)
|
||||
|
||||
upload_root = current_app.config["UPLOAD_FOLDER"]
|
||||
used_bytes = get_company_upload_bytes(upload_root, company.id)
|
||||
usage = compute_storage_usage(used_bytes=used_bytes, max_bytes=company.storage_max_bytes)
|
||||
storage_max_human = _format_bytes(usage["max_bytes"]) if usage.get("max_bytes") else None
|
||||
|
||||
if usage.get("is_exceeded"):
|
||||
return _json_error(_storage_limit_error_message(storage_max_human=storage_max_human), 403)
|
||||
|
||||
# Determine starting position.
|
||||
max_pos = (
|
||||
db.session.query(db.func.max(PlaylistItem.position)).filter_by(playlist_id=playlist_id).scalar() or 0
|
||||
)
|
||||
|
||||
saved_relpaths: list[str] = []
|
||||
items: list[PlaylistItem] = []
|
||||
|
||||
try:
|
||||
for idx, f in enumerate(files, start=1):
|
||||
if not f or not f.filename:
|
||||
raise ValueError("file_required")
|
||||
|
||||
filename = secure_filename(f.filename)
|
||||
ext = os.path.splitext(filename)[1].lower()
|
||||
if ext not in ALLOWED_IMAGE_EXTENSIONS:
|
||||
raise ValueError("unsupported")
|
||||
|
||||
relpath = _save_compressed_image(
|
||||
f,
|
||||
current_app.config["UPLOAD_FOLDER"],
|
||||
current_user.company_id,
|
||||
crop_mode=crop_mode,
|
||||
)
|
||||
saved_relpaths.append(relpath)
|
||||
|
||||
it = PlaylistItem(
|
||||
playlist=playlist,
|
||||
item_type="image",
|
||||
title=None,
|
||||
duration_seconds=duration,
|
||||
position=max_pos + idx,
|
||||
file_path=relpath,
|
||||
)
|
||||
items.append(it)
|
||||
|
||||
# Post-save quota check (like single image uploads)
|
||||
if company.storage_max_bytes is not None and int(company.storage_max_bytes or 0) > 0:
|
||||
used_after = get_company_upload_bytes(upload_root, company.id)
|
||||
usage_after = compute_storage_usage(used_bytes=used_after, max_bytes=company.storage_max_bytes)
|
||||
if usage_after.get("is_exceeded"):
|
||||
# Remove all newly saved files and reject.
|
||||
for p in saved_relpaths:
|
||||
_try_delete_upload(p, upload_root)
|
||||
return _json_error(_storage_limit_error_message(storage_max_human=storage_max_human), 403)
|
||||
|
||||
for it in items:
|
||||
db.session.add(it)
|
||||
db.session.commit()
|
||||
except ValueError as e:
|
||||
# Clean up any saved files.
|
||||
upload_root = current_app.config["UPLOAD_FOLDER"]
|
||||
for p in saved_relpaths:
|
||||
_try_delete_upload(p, upload_root)
|
||||
|
||||
code = str(e)
|
||||
if code == "unsupported":
|
||||
return _json_error(
|
||||
"Unsupported image type. Please upload one of: " + ", ".join(sorted(ALLOWED_IMAGE_EXTENSIONS))
|
||||
)
|
||||
if code == "file_required":
|
||||
return _json_error("File required")
|
||||
return _json_error("Failed to process image upload", 500)
|
||||
except Exception:
|
||||
db.session.rollback()
|
||||
upload_root = current_app.config["UPLOAD_FOLDER"]
|
||||
for p in saved_relpaths:
|
||||
_try_delete_upload(p, upload_root)
|
||||
return _json_error("Failed to process image upload", 500)
|
||||
|
||||
return jsonify(
|
||||
{
|
||||
"ok": True,
|
||||
"items": [
|
||||
{
|
||||
"id": it.id,
|
||||
"playlist_id": it.playlist_id,
|
||||
"position": it.position,
|
||||
"item_type": it.item_type,
|
||||
"title": it.title,
|
||||
"file_path": it.file_path,
|
||||
"url": it.url,
|
||||
"duration_seconds": it.duration_seconds,
|
||||
}
|
||||
for it in items
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@bp.post("/items/<int:item_id>/delete")
|
||||
@login_required
|
||||
def delete_item(item_id: int):
|
||||
|
||||
Reference in New Issue
Block a user