255 lines
8.3 KiB
Python
255 lines
8.3 KiB
Python
import os
|
|
import uuid
|
|
|
|
from flask import Blueprint, abort, current_app, flash, redirect, render_template, request, url_for
|
|
from flask_login import current_user, login_required
|
|
from werkzeug.utils import secure_filename
|
|
|
|
from PIL import Image
|
|
|
|
from ..extensions import db
|
|
from ..models import Display, Playlist, PlaylistItem
|
|
|
|
|
|
def _save_compressed_image(uploaded_file, upload_folder: str) -> str:
|
|
"""Save an uploaded image as a compressed WEBP file.
|
|
|
|
Returns relative file path under /static (e.g. uploads/<uuid>.webp)
|
|
"""
|
|
|
|
unique = f"{uuid.uuid4().hex}.webp"
|
|
save_path = os.path.join(upload_folder, unique)
|
|
|
|
img = Image.open(uploaded_file)
|
|
# Normalize mode for webp
|
|
if img.mode not in ("RGB", "RGBA"):
|
|
img = img.convert("RGB")
|
|
|
|
# Resize down if very large (keeps aspect ratio)
|
|
img.thumbnail((1920, 1080))
|
|
|
|
img.save(save_path, format="WEBP", quality=80, method=6)
|
|
return f"uploads/{unique}"
|
|
|
|
|
|
def _try_delete_upload(file_path: str | None, upload_folder: str):
|
|
"""Best-effort delete of an uploaded media file."""
|
|
if not file_path:
|
|
return
|
|
if not file_path.startswith("uploads/"):
|
|
return
|
|
filename = file_path.split("/", 1)[1]
|
|
abs_path = os.path.join(upload_folder, filename)
|
|
try:
|
|
if os.path.isfile(abs_path):
|
|
os.remove(abs_path)
|
|
except Exception:
|
|
# Ignore cleanup failures
|
|
pass
|
|
|
|
bp = Blueprint("company", __name__, url_prefix="/company")
|
|
|
|
|
|
def company_user_required():
|
|
if not current_user.is_authenticated:
|
|
abort(403)
|
|
if current_user.is_admin:
|
|
abort(403)
|
|
if not current_user.company_id:
|
|
abort(403)
|
|
|
|
|
|
@bp.get("/")
|
|
@login_required
|
|
def dashboard():
|
|
company_user_required()
|
|
playlists = Playlist.query.filter_by(company_id=current_user.company_id).order_by(Playlist.name.asc()).all()
|
|
displays = Display.query.filter_by(company_id=current_user.company_id).order_by(Display.name.asc()).all()
|
|
return render_template("company/dashboard.html", playlists=playlists, displays=displays)
|
|
|
|
|
|
@bp.post("/playlists")
|
|
@login_required
|
|
def create_playlist():
|
|
company_user_required()
|
|
name = request.form.get("name", "").strip()
|
|
if not name:
|
|
flash("Playlist name required", "danger")
|
|
return redirect(url_for("company.dashboard"))
|
|
p = Playlist(company_id=current_user.company_id, name=name)
|
|
db.session.add(p)
|
|
db.session.commit()
|
|
flash("Playlist created", "success")
|
|
return redirect(url_for("company.playlist_detail", playlist_id=p.id))
|
|
|
|
|
|
@bp.get("/playlists/<int:playlist_id>")
|
|
@login_required
|
|
def playlist_detail(playlist_id: int):
|
|
company_user_required()
|
|
playlist = db.session.get(Playlist, playlist_id)
|
|
if not playlist or playlist.company_id != current_user.company_id:
|
|
abort(404)
|
|
return render_template("company/playlist_detail.html", playlist=playlist)
|
|
|
|
|
|
@bp.post("/playlists/<int:playlist_id>/delete")
|
|
@login_required
|
|
def delete_playlist(playlist_id: int):
|
|
company_user_required()
|
|
playlist = db.session.get(Playlist, playlist_id)
|
|
if not playlist or playlist.company_id != current_user.company_id:
|
|
abort(404)
|
|
|
|
# Unassign from any displays in this company
|
|
Display.query.filter_by(company_id=current_user.company_id, assigned_playlist_id=playlist.id).update(
|
|
{"assigned_playlist_id": None}
|
|
)
|
|
|
|
# cleanup uploaded files for image/video items
|
|
for it in list(playlist.items):
|
|
if it.item_type in ("image", "video"):
|
|
_try_delete_upload(it.file_path, current_app.config["UPLOAD_FOLDER"])
|
|
|
|
db.session.delete(playlist)
|
|
db.session.commit()
|
|
flash("Playlist deleted", "success")
|
|
return redirect(url_for("company.dashboard"))
|
|
|
|
|
|
@bp.post("/playlists/<int:playlist_id>/items/reorder")
|
|
@login_required
|
|
def reorder_playlist_items(playlist_id: int):
|
|
"""Persist new ordering for playlist items.
|
|
|
|
Expects form data: order=<comma-separated item ids>.
|
|
"""
|
|
company_user_required()
|
|
playlist = db.session.get(Playlist, playlist_id)
|
|
if not playlist or playlist.company_id != current_user.company_id:
|
|
abort(404)
|
|
|
|
order = (request.form.get("order") or "").strip()
|
|
if not order:
|
|
abort(400)
|
|
|
|
try:
|
|
ids = [int(x) for x in order.split(",") if x.strip()]
|
|
except ValueError:
|
|
abort(400)
|
|
|
|
# Ensure ids belong to this playlist
|
|
existing = PlaylistItem.query.filter(PlaylistItem.playlist_id == playlist_id, PlaylistItem.id.in_(ids)).all()
|
|
existing_ids = {i.id for i in existing}
|
|
if len(existing_ids) != len(ids):
|
|
abort(400)
|
|
|
|
# Re-number positions starting at 1
|
|
id_to_item = {i.id: i for i in existing}
|
|
for pos, item_id in enumerate(ids, start=1):
|
|
id_to_item[item_id].position = pos
|
|
|
|
db.session.commit()
|
|
return ("", 204)
|
|
|
|
|
|
@bp.post("/playlists/<int:playlist_id>/items")
|
|
@login_required
|
|
def add_playlist_item(playlist_id: int):
|
|
company_user_required()
|
|
playlist = db.session.get(Playlist, playlist_id)
|
|
if not playlist or playlist.company_id != current_user.company_id:
|
|
abort(404)
|
|
|
|
item_type = request.form.get("item_type")
|
|
title = request.form.get("title", "").strip() or None
|
|
duration = int(request.form.get("duration_seconds") or 10)
|
|
|
|
max_pos = (
|
|
db.session.query(db.func.max(PlaylistItem.position)).filter_by(playlist_id=playlist_id).scalar() or 0
|
|
)
|
|
pos = max_pos + 1
|
|
|
|
item = PlaylistItem(
|
|
playlist=playlist,
|
|
item_type=item_type,
|
|
title=title,
|
|
duration_seconds=max(1, duration),
|
|
position=pos,
|
|
)
|
|
|
|
if item_type in ("image", "video"):
|
|
f = request.files.get("file")
|
|
if not f or not f.filename:
|
|
flash("File required", "danger")
|
|
return redirect(url_for("company.playlist_detail", playlist_id=playlist_id))
|
|
|
|
filename = secure_filename(f.filename)
|
|
ext = os.path.splitext(filename)[1].lower()
|
|
|
|
if item_type == "image" and ext in (".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".tiff"):
|
|
try:
|
|
item.file_path = _save_compressed_image(f, current_app.config["UPLOAD_FOLDER"])
|
|
except Exception:
|
|
flash("Failed to process image upload", "danger")
|
|
return redirect(url_for("company.playlist_detail", playlist_id=playlist_id))
|
|
else:
|
|
# Videos (and unknown image types): keep as-is but always rename to a UUID
|
|
unique = uuid.uuid4().hex + ext
|
|
save_path = os.path.join(current_app.config["UPLOAD_FOLDER"], unique)
|
|
f.save(save_path)
|
|
item.file_path = f"uploads/{unique}"
|
|
|
|
elif item_type == "webpage":
|
|
url = request.form.get("url", "").strip()
|
|
if not url:
|
|
flash("URL required", "danger")
|
|
return redirect(url_for("company.playlist_detail", playlist_id=playlist_id))
|
|
item.url = url
|
|
else:
|
|
flash("Invalid item type", "danger")
|
|
return redirect(url_for("company.playlist_detail", playlist_id=playlist_id))
|
|
|
|
db.session.add(item)
|
|
db.session.commit()
|
|
flash("Item added", "success")
|
|
return redirect(url_for("company.playlist_detail", playlist_id=playlist_id))
|
|
|
|
|
|
@bp.post("/items/<int:item_id>/delete")
|
|
@login_required
|
|
def delete_item(item_id: int):
|
|
company_user_required()
|
|
item = db.session.get(PlaylistItem, item_id)
|
|
if not item or item.playlist.company_id != current_user.company_id:
|
|
abort(404)
|
|
playlist_id = item.playlist_id
|
|
|
|
if item.item_type in ("image", "video"):
|
|
_try_delete_upload(item.file_path, current_app.config["UPLOAD_FOLDER"])
|
|
|
|
db.session.delete(item)
|
|
db.session.commit()
|
|
flash("Item deleted", "success")
|
|
return redirect(url_for("company.playlist_detail", playlist_id=playlist_id))
|
|
|
|
|
|
@bp.post("/displays/<int:display_id>/assign")
|
|
@login_required
|
|
def assign_playlist(display_id: int):
|
|
company_user_required()
|
|
display = db.session.get(Display, display_id)
|
|
if not display or display.company_id != current_user.company_id:
|
|
abort(404)
|
|
playlist_id = request.form.get("playlist_id")
|
|
if not playlist_id:
|
|
display.assigned_playlist_id = None
|
|
else:
|
|
playlist = db.session.get(Playlist, int(playlist_id))
|
|
if not playlist or playlist.company_id != current_user.company_id:
|
|
abort(400)
|
|
display.assigned_playlist_id = playlist.id
|
|
db.session.commit()
|
|
flash("Display assignment updated", "success")
|
|
return redirect(url_for("company.dashboard"))
|