mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-13 13:53:12 -06:00
fix(server): floor duration_sec to prevent widget zero-duration player loop (#199)
A duration_sec=0 assignment (especially a widget) made the player schedule a 0ms auto-advance, self-looping and black-screening the TV. #198 fixed the Android client; this hardens the source so a 0 can't be stored or served in the first place. assignments.js accepted an explicit 0 on the POST/PUT/copy write paths — the `= 10` destructure default only covers an ABSENT field, not an explicit 0. - Add normalizeDuration() and apply it on all assignment write paths so any missing/invalid/<1 duration is floored to the 10s default. - Add an idempotent migration repairing existing playlist_items rows with duration_sec IS NULL OR < 1 (fixes the live widget on existing DBs). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
af286826dc
commit
5c1cb4b992
|
|
@ -358,6 +358,11 @@ const migrations = [
|
|||
last_seen INTEGER,
|
||||
removed_at INTEGER
|
||||
)`,
|
||||
// #widget zero-duration loop: repair any playlist_items with a non-positive duration
|
||||
// (esp. duration_sec=0 on a widget), which made the player schedule a 0ms auto-advance
|
||||
// -> self-loop + black screen. New writes are floored in routes/assignments.js; this
|
||||
// fixes existing rows. Idempotent — a no-op once clean.
|
||||
'UPDATE playlist_items SET duration_sec = 10 WHERE duration_sec IS NULL OR duration_sec < 1',
|
||||
];
|
||||
// Apply each ALTER idempotently. A "duplicate column name" / "already exists"
|
||||
// error means the column is already present (expected on a migrated DB) - benign.
|
||||
|
|
|
|||
|
|
@ -14,6 +14,15 @@ function markDraft(playlistId) {
|
|||
db.prepare("UPDATE playlists SET status = 'draft', updated_at = strftime('%s','now') WHERE id = ?").run(playlistId);
|
||||
}
|
||||
|
||||
// Hardening (#widget zero-duration loop): a non-positive duration — especially
|
||||
// duration_sec=0 on a widget — makes the player schedule a 0ms auto-advance, which
|
||||
// self-loops and black-screens the TV. Never STORE a bad value: floor any missing/
|
||||
// invalid/<1 duration to the 10s default so it can't reach a device.
|
||||
function normalizeDuration(v) {
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) && n >= 1 ? Math.floor(n) : 10;
|
||||
}
|
||||
|
||||
// Hardening (#zone-orphan): a zone_id only renders if it belongs to the layout the
|
||||
// device is actually showing. Assigning a zone from a DIFFERENT layout (e.g. after a
|
||||
// layout switch/duplicate) creates an item that the players can't place. We CLEAR a
|
||||
|
|
@ -95,7 +104,8 @@ router.get('/device/:deviceId', (req, res) => {
|
|||
router.post('/device/:deviceId', (req, res) => {
|
||||
const access = checkDeviceAccess(req, res, 'deviceId', true);
|
||||
if (!access) return;
|
||||
const { content_id, widget_id, zone_id, duration_sec = 10, sort_order } = req.body;
|
||||
const { content_id, widget_id, zone_id, sort_order } = req.body;
|
||||
const duration_sec = normalizeDuration(req.body.duration_sec);
|
||||
|
||||
if (!content_id && !widget_id) return res.status(400).json({ error: 'content_id or widget_id required' });
|
||||
|
||||
|
|
@ -224,7 +234,7 @@ router.put('/:id', (req, res) => {
|
|||
const values = [];
|
||||
|
||||
if (sort_order !== undefined) { updates.push('sort_order = ?'); values.push(sort_order); }
|
||||
if (duration_sec !== undefined) { updates.push('duration_sec = ?'); values.push(duration_sec); }
|
||||
if (duration_sec !== undefined) { updates.push('duration_sec = ?'); values.push(normalizeDuration(duration_sec)); }
|
||||
// zone_id can be null (clear the zone) - treat undefined as "no change",
|
||||
// any other value (including null) as "write this".
|
||||
if (zone_id !== undefined) {
|
||||
|
|
@ -327,7 +337,7 @@ router.post('/device/:deviceId/copy-to/:targetDeviceId', (req, res) => {
|
|||
|
||||
const transaction = db.transaction(() => {
|
||||
sourceItems.forEach((a, i) => {
|
||||
stmt.run(targetPlaylistId, a.content_id, a.widget_id, a.zone_id || null, maxOrder + i + 1, a.duration_sec);
|
||||
stmt.run(targetPlaylistId, a.content_id, a.widget_id, a.zone_id || null, maxOrder + i + 1, normalizeDuration(a.duration_sec));
|
||||
});
|
||||
});
|
||||
transaction();
|
||||
|
|
|
|||
Loading…
Reference in a new issue