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/.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/") @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//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//items/reorder") @login_required def reorder_playlist_items(playlist_id: int): """Persist new ordering for playlist items. Expects form data: order=. """ 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//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//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//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"))