Update app
This commit is contained in:
@@ -7,7 +7,7 @@ from flask_login import current_user, login_required, login_user
|
||||
|
||||
from ..extensions import db
|
||||
from ..uploads import abs_upload_path, ensure_company_upload_dir, get_company_upload_bytes, is_valid_upload_relpath
|
||||
from ..models import AppSettings, Company, Display, DisplaySession, Playlist, PlaylistItem, User
|
||||
from ..models import AppSettings, Company, Display, DisplayPlaylist, DisplaySession, Playlist, PlaylistItem, User
|
||||
from ..email_utils import send_email
|
||||
|
||||
bp = Blueprint("admin", __name__, url_prefix="/admin")
|
||||
@@ -375,8 +375,12 @@ def delete_company(company_id: int):
|
||||
for d in list(company.displays):
|
||||
d.assigned_playlist_id = None
|
||||
|
||||
# 2) Delete display sessions referencing displays of this company
|
||||
# 1b) Clear multi-playlist mappings
|
||||
display_ids = [d.id for d in company.displays]
|
||||
if display_ids:
|
||||
DisplayPlaylist.query.filter(DisplayPlaylist.display_id.in_(display_ids)).delete(synchronize_session=False)
|
||||
|
||||
# 2) Delete display sessions referencing displays of this company
|
||||
if display_ids:
|
||||
DisplaySession.query.filter(DisplaySession.display_id.in_(display_ids)).delete(synchronize_session=False)
|
||||
|
||||
@@ -518,10 +522,13 @@ def delete_display(display_id: int):
|
||||
# 1) Unassign playlist
|
||||
display.assigned_playlist_id = None
|
||||
|
||||
# 2) Delete active sessions for this display
|
||||
# 2) Clear multi-playlist mappings
|
||||
DisplayPlaylist.query.filter_by(display_id=display.id).delete(synchronize_session=False)
|
||||
|
||||
# 3) Delete active sessions for this display
|
||||
DisplaySession.query.filter_by(display_id=display.id).delete(synchronize_session=False)
|
||||
|
||||
# 3) Delete display
|
||||
# 4) Delete display
|
||||
db.session.delete(display)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@@ -6,12 +6,12 @@ import time
|
||||
from flask import Blueprint, Response, abort, jsonify, request, stream_with_context, url_for
|
||||
|
||||
from ..extensions import db
|
||||
from ..models import Display, DisplaySession
|
||||
from ..models import Display, DisplayPlaylist, DisplaySession, Playlist, PlaylistItem
|
||||
|
||||
bp = Blueprint("api", __name__, url_prefix="/api")
|
||||
|
||||
|
||||
MAX_ACTIVE_SESSIONS_PER_DISPLAY = 2
|
||||
MAX_ACTIVE_SESSIONS_PER_DISPLAY = 3
|
||||
SESSION_TTL_SECONDS = 90
|
||||
|
||||
|
||||
@@ -84,16 +84,49 @@ def _playlist_signature(display: Display) -> tuple[int | None, str]:
|
||||
duration changes, and item adds/deletes trigger an update.
|
||||
"""
|
||||
|
||||
playlist = display.assigned_playlist
|
||||
if not playlist:
|
||||
# Determine active playlists. If display_playlist has any rows, use those.
|
||||
# Otherwise fall back to the legacy assigned_playlist_id.
|
||||
mapped_ids = [
|
||||
r[0]
|
||||
for r in db.session.query(DisplayPlaylist.playlist_id)
|
||||
.filter(DisplayPlaylist.display_id == display.id)
|
||||
.order_by(DisplayPlaylist.position.asc(), DisplayPlaylist.playlist_id.asc())
|
||||
.all()
|
||||
]
|
||||
use_mapping = bool(mapped_ids)
|
||||
active_ids = mapped_ids
|
||||
if not active_ids and display.assigned_playlist_id:
|
||||
active_ids = [display.assigned_playlist_id]
|
||||
use_mapping = False
|
||||
|
||||
if not active_ids:
|
||||
raw = "no-playlist"
|
||||
return None, hashlib.sha1(raw.encode("utf-8")).hexdigest()
|
||||
|
||||
# Pull items in a stable order so reordering affects signature.
|
||||
if use_mapping:
|
||||
items = (
|
||||
PlaylistItem.query.join(DisplayPlaylist, DisplayPlaylist.playlist_id == PlaylistItem.playlist_id)
|
||||
.filter(
|
||||
DisplayPlaylist.display_id == display.id,
|
||||
PlaylistItem.playlist_id.in_(active_ids),
|
||||
)
|
||||
.order_by(DisplayPlaylist.position.asc(), PlaylistItem.position.asc())
|
||||
.all()
|
||||
)
|
||||
else:
|
||||
items = (
|
||||
PlaylistItem.query.filter(PlaylistItem.playlist_id == active_ids[0])
|
||||
.order_by(PlaylistItem.position.asc())
|
||||
.all()
|
||||
)
|
||||
|
||||
payload = {
|
||||
"playlist_id": playlist.id,
|
||||
"playlist_ids": list(active_ids),
|
||||
"items": [
|
||||
{
|
||||
"id": it.id,
|
||||
"playlist_id": it.playlist_id,
|
||||
"pos": it.position,
|
||||
"type": it.item_type,
|
||||
"title": it.title,
|
||||
@@ -101,11 +134,15 @@ def _playlist_signature(display: Display) -> tuple[int | None, str]:
|
||||
"file_path": it.file_path,
|
||||
"url": it.url,
|
||||
}
|
||||
for it in playlist.items
|
||||
for it in items
|
||||
],
|
||||
}
|
||||
raw = json.dumps(payload, sort_keys=True, separators=(",", ":"))
|
||||
return playlist.id, hashlib.sha1(raw.encode("utf-8")).hexdigest()
|
||||
# signature returns a single playlist_id previously; now return None when multiple.
|
||||
# callers only use it for changed-detection.
|
||||
if len(set(active_ids)) == 1:
|
||||
return active_ids[0], hashlib.sha1(raw.encode("utf-8")).hexdigest()
|
||||
return None, hashlib.sha1(raw.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
@bp.get("/display/<token>/playlist")
|
||||
@@ -114,21 +151,59 @@ def display_playlist(token: str):
|
||||
if not display:
|
||||
abort(404)
|
||||
|
||||
# Enforce: a display URL/token can be opened by max 2 concurrently active sessions.
|
||||
# Enforce: a display URL/token can be opened by max 3 concurrently active sessions.
|
||||
# Player sends a stable `sid` via querystring.
|
||||
sid = request.args.get("sid")
|
||||
ok, resp = _enforce_and_touch_display_session(display, sid)
|
||||
if not ok:
|
||||
return resp
|
||||
|
||||
playlist = display.assigned_playlist
|
||||
if not playlist:
|
||||
return jsonify({"display": display.name, "playlist": None, "items": []})
|
||||
# Determine active playlists. If display_playlist has any rows, use those.
|
||||
# Otherwise fall back to the legacy assigned_playlist_id.
|
||||
mapped_ids = [
|
||||
r[0]
|
||||
for r in db.session.query(DisplayPlaylist.playlist_id)
|
||||
.filter(DisplayPlaylist.display_id == display.id)
|
||||
.order_by(DisplayPlaylist.position.asc(), DisplayPlaylist.playlist_id.asc())
|
||||
.all()
|
||||
]
|
||||
use_mapping = bool(mapped_ids)
|
||||
active_ids = mapped_ids
|
||||
if not active_ids and display.assigned_playlist_id:
|
||||
active_ids = [display.assigned_playlist_id]
|
||||
use_mapping = False
|
||||
|
||||
if not active_ids:
|
||||
return jsonify({"display": display.name, "playlists": [], "items": []})
|
||||
|
||||
playlists = Playlist.query.filter(Playlist.id.in_(active_ids)).all()
|
||||
pl_by_id = {p.id: p for p in playlists}
|
||||
ordered_playlists = [pl_by_id[x] for x in active_ids if x in pl_by_id]
|
||||
|
||||
# Merge items across active playlists.
|
||||
if use_mapping:
|
||||
merged = (
|
||||
PlaylistItem.query.join(DisplayPlaylist, DisplayPlaylist.playlist_id == PlaylistItem.playlist_id)
|
||||
.filter(
|
||||
DisplayPlaylist.display_id == display.id,
|
||||
PlaylistItem.playlist_id.in_(active_ids),
|
||||
)
|
||||
.order_by(DisplayPlaylist.position.asc(), PlaylistItem.position.asc())
|
||||
.all()
|
||||
)
|
||||
else:
|
||||
merged = (
|
||||
PlaylistItem.query.filter(PlaylistItem.playlist_id == active_ids[0])
|
||||
.order_by(PlaylistItem.position.asc())
|
||||
.all()
|
||||
)
|
||||
|
||||
items = []
|
||||
for item in playlist.items:
|
||||
for item in merged:
|
||||
payload = {
|
||||
"id": item.id,
|
||||
"playlist_id": item.playlist_id,
|
||||
"playlist_name": (pl_by_id.get(item.playlist_id).name if pl_by_id.get(item.playlist_id) else None),
|
||||
"type": item.item_type,
|
||||
"title": item.title,
|
||||
"duration": item.duration_seconds,
|
||||
@@ -142,7 +217,7 @@ def display_playlist(token: str):
|
||||
return jsonify(
|
||||
{
|
||||
"display": display.name,
|
||||
"playlist": {"id": playlist.id, "name": playlist.name},
|
||||
"playlists": [{"id": p.id, "name": p.name} for p in ordered_playlists],
|
||||
"items": items,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -18,7 +18,7 @@ from ..uploads import (
|
||||
get_company_upload_bytes,
|
||||
is_valid_upload_relpath,
|
||||
)
|
||||
from ..models import AppSettings, Company, Display, DisplaySession, Playlist, PlaylistItem, User
|
||||
from ..models import AppSettings, Company, Display, DisplayPlaylist, DisplaySession, Playlist, PlaylistItem, User
|
||||
from ..email_utils import send_email
|
||||
from ..auth_tokens import make_password_reset_token
|
||||
|
||||
@@ -343,7 +343,13 @@ 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)
|
||||
playlists_json = [{"id": p.id, "name": p.name} for p in playlists]
|
||||
return render_template(
|
||||
"company/dashboard.html",
|
||||
playlists=playlists,
|
||||
playlists_json=playlists_json,
|
||||
displays=displays,
|
||||
)
|
||||
|
||||
|
||||
@bp.post("/playlists")
|
||||
@@ -412,6 +418,15 @@ def delete_playlist(playlist_id: int):
|
||||
{"assigned_playlist_id": None}
|
||||
)
|
||||
|
||||
# Remove from any display multi-playlist mappings in this company.
|
||||
# Use a subquery to avoid a JOIN-based DELETE which is not supported on SQLite.
|
||||
display_ids = [d.id for d in Display.query.filter_by(company_id=current_user.company_id).all()]
|
||||
if display_ids:
|
||||
DisplayPlaylist.query.filter(
|
||||
DisplayPlaylist.display_id.in_(display_ids),
|
||||
DisplayPlaylist.playlist_id == playlist.id,
|
||||
).delete(synchronize_session=False)
|
||||
|
||||
# cleanup uploaded files for image/video items
|
||||
for it in list(playlist.items):
|
||||
if it.item_type in ("image", "video"):
|
||||
@@ -900,3 +915,87 @@ def update_display(display_id: int):
|
||||
|
||||
flash("Display updated", "success")
|
||||
return redirect(url_for("company.dashboard"))
|
||||
|
||||
|
||||
@bp.post("/displays/<int:display_id>/playlists")
|
||||
@login_required
|
||||
def update_display_playlists(display_id: int):
|
||||
"""Set active playlists for a display.
|
||||
|
||||
Expects JSON: { playlist_ids: [1,2,3] }
|
||||
Returns JSON with the updated assigned playlist ids.
|
||||
|
||||
Note: if playlist_ids is empty, the display will have no active playlists.
|
||||
For backwards compatibility, this does NOT modify Display.assigned_playlist_id.
|
||||
"""
|
||||
|
||||
company_user_required()
|
||||
|
||||
display = db.session.get(Display, display_id)
|
||||
if not display or display.company_id != current_user.company_id:
|
||||
abort(404)
|
||||
|
||||
if not request.is_json:
|
||||
abort(400)
|
||||
|
||||
payload = request.get_json(silent=True) or {}
|
||||
raw_ids = payload.get("playlist_ids")
|
||||
if raw_ids is None:
|
||||
return jsonify({"ok": False, "error": "playlist_ids is required"}), 400
|
||||
|
||||
if not isinstance(raw_ids, list):
|
||||
return jsonify({"ok": False, "error": "playlist_ids must be a list"}), 400
|
||||
|
||||
playlist_ids: list[int] = []
|
||||
try:
|
||||
for x in raw_ids:
|
||||
if x in (None, ""):
|
||||
continue
|
||||
playlist_ids.append(int(x))
|
||||
except (TypeError, ValueError):
|
||||
return jsonify({"ok": False, "error": "Invalid playlist id"}), 400
|
||||
|
||||
# Ensure playlists belong to this company.
|
||||
if playlist_ids:
|
||||
allowed = {
|
||||
p.id
|
||||
for p in Playlist.query.filter(
|
||||
Playlist.company_id == current_user.company_id,
|
||||
Playlist.id.in_(playlist_ids),
|
||||
).all()
|
||||
}
|
||||
if len(allowed) != len(set(playlist_ids)):
|
||||
return jsonify({"ok": False, "error": "One or more playlists are invalid"}), 400
|
||||
|
||||
# Replace mapping rows.
|
||||
DisplayPlaylist.query.filter_by(display_id=display.id).delete(synchronize_session=False)
|
||||
now = datetime.utcnow()
|
||||
for pos, pid in enumerate(dict.fromkeys(playlist_ids), start=1):
|
||||
db.session.add(
|
||||
DisplayPlaylist(
|
||||
display_id=display.id,
|
||||
playlist_id=pid,
|
||||
position=pos,
|
||||
created_at=now,
|
||||
)
|
||||
)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
active_ids = [
|
||||
r[0]
|
||||
for r in db.session.query(DisplayPlaylist.playlist_id)
|
||||
.filter(DisplayPlaylist.display_id == display.id)
|
||||
.order_by(DisplayPlaylist.position.asc(), DisplayPlaylist.playlist_id.asc())
|
||||
.all()
|
||||
]
|
||||
|
||||
return jsonify(
|
||||
{
|
||||
"ok": True,
|
||||
"display": {
|
||||
"id": display.id,
|
||||
"active_playlist_ids": active_ids,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user