305 lines
11 KiB
TypeScript
305 lines
11 KiB
TypeScript
import { randomUUID } from 'node:crypto';
|
|
import { error, fail } from '@sveltejs/kit';
|
|
import type { Actions, PageServerLoad } from './$types';
|
|
import { requireBarkeeper, requireUser } from '#lib/server/auth.js';
|
|
import { finalizeShow, getCompletionSummary } from '#lib/server/completion.js';
|
|
import {
|
|
completeCurrent,
|
|
clearPresentationChannel,
|
|
endLive,
|
|
getLiveView,
|
|
goBack,
|
|
issuePresentationAccess,
|
|
moveOpenUsage,
|
|
pauseLive,
|
|
presentUsage,
|
|
rotatePresentationChannelKey,
|
|
resetSectionTimer,
|
|
resetTotalTimer,
|
|
resumeEndedLive,
|
|
resumeLive,
|
|
selectUsage,
|
|
setShowStatus,
|
|
skipCurrent,
|
|
startNewLiveRun,
|
|
startOrResumeLive
|
|
} from '#lib/server/live.js';
|
|
import { listShows } from '#lib/server/shows.js';
|
|
import { createShare, deleteShare, listShares, updateShare } from '#lib/server/shares.js';
|
|
import { berlinLocalToUtc } from '#lib/server/time.js';
|
|
|
|
function text(data: FormData, name: string) {
|
|
return String(data.get(name) || '');
|
|
}
|
|
|
|
function actionError(cause: unknown) {
|
|
const code = cause instanceof Error ? cause.message : '';
|
|
if (code === 'CONFLICT')
|
|
return {
|
|
status: 409,
|
|
error: 'Der Live-Stand wurde bereits geändert. Die Ansicht wurde erneuert.'
|
|
};
|
|
if (code === 'NO_CURRENT') return { status: 409, error: 'Es ist kein aktuelles Piece aktiv.' };
|
|
if (code === 'NOT_STARTED')
|
|
return { status: 409, error: 'Die Sendung muss vor der Auswahl gestartet werden.' };
|
|
if (code === 'NO_PREVIOUS') return { status: 409, error: 'Es gibt kein vorheriges Piece.' };
|
|
if (code === 'LIVE_ORDER_LOCKED')
|
|
return {
|
|
status: 409,
|
|
error: 'Nur offene Pieces hinter dem aktuellen Stand können sortiert werden.'
|
|
};
|
|
if (code === 'SHOW_COMPLETED')
|
|
return { status: 409, error: 'Abgeschlossene Sendungen können nicht live gesteuert werden.' };
|
|
if (code === 'NOT_FOUND')
|
|
return { status: 404, error: 'Sendung oder Piece wurde nicht gefunden.' };
|
|
if (code === 'TIMER_PAUSED') return { status: 409, error: 'Die Sendung ist pausiert.' };
|
|
if (code === 'ALREADY_PAUSED' || code === 'ALREADY_RUNNING')
|
|
return { status: 409, error: 'Der Timerzustand wurde bereits geändert.' };
|
|
if (code === 'NO_SECTION_TIMER')
|
|
return { status: 409, error: 'Es ist kein Abschnittstimer aktiv.' };
|
|
if (code === 'NOT_LIVE' || code === 'NOT_ENDED' || code === 'ALREADY_STARTED')
|
|
return { status: 409, error: 'Die Sendung befindet sich nicht im passenden Zustand.' };
|
|
if (code.startsWith('INVALID_')) return { status: 400, error: 'Die Live-Aktion ist ungültig.' };
|
|
return { status: 500, error: 'Die Live-Aktion konnte nicht gespeichert werden.' };
|
|
}
|
|
|
|
function attempt(intent: string, operation: () => unknown) {
|
|
try {
|
|
return { intent, success: true, result: operation() };
|
|
} catch (cause) {
|
|
const problem = actionError(cause);
|
|
return fail(problem.status, { intent, ...problem });
|
|
}
|
|
}
|
|
|
|
export const load: PageServerLoad = ({ locals, url }) => {
|
|
const actor = requireUser(locals.user);
|
|
const showId = url.searchParams.get('show') || listShows()[0]?.id;
|
|
if (!showId) error(404, 'Keine Sendung vorhanden');
|
|
try {
|
|
const live = getLiveView(showId);
|
|
return {
|
|
live,
|
|
canManageShares: actor.role === 'barkeeper',
|
|
shares: actor.role === 'barkeeper' ? listShares(actor, showId) : [],
|
|
commandId: randomUUID(),
|
|
completion:
|
|
live.endedAtUtc && live.show.status !== 'completed' ? getCompletionSummary(showId) : null
|
|
};
|
|
} catch (cause) {
|
|
if (cause instanceof Error && cause.message === 'NOT_FOUND')
|
|
error(404, 'Sendung nicht gefunden');
|
|
throw cause;
|
|
}
|
|
};
|
|
|
|
export const actions = {
|
|
createShare: async ({ locals, request }) => {
|
|
const actor = requireBarkeeper(locals.user);
|
|
const data = await request.formData();
|
|
try {
|
|
const share = await createShare(actor, {
|
|
showId: text(data, 'showId'), scope: text(data, 'scope'), password: text(data, 'password'),
|
|
expiresAtUtc: berlinLocalToUtc(text(data, 'expiresAt'), 'INVALID_EXPIRY')
|
|
});
|
|
return { intent: 'createShare', success: 'Leselink angelegt.', shareUrl: `/freigabe/${share.token}` };
|
|
} catch (cause) {
|
|
const problem = actionError(cause);
|
|
return fail(problem.status, { intent: 'createShare', ...problem });
|
|
}
|
|
},
|
|
updateShare: async ({ locals, request }) => {
|
|
const actor = requireBarkeeper(locals.user);
|
|
const data = await request.formData();
|
|
try {
|
|
await updateShare(actor, text(data, 'shareId'), {
|
|
scope: text(data, 'scope'), passwordMode: text(data, 'passwordMode'), password: text(data, 'password'),
|
|
expiresAtUtc: berlinLocalToUtc(text(data, 'expiresAt'), 'INVALID_EXPIRY')
|
|
}, text(data, 'showId'));
|
|
return { intent: 'updateShare', success: 'Freigabe aktualisiert.' };
|
|
} catch (cause) {
|
|
const problem = actionError(cause);
|
|
return fail(problem.status, { intent: 'updateShare', ...problem });
|
|
}
|
|
},
|
|
deleteShare: async ({ locals, request }) => {
|
|
const actor = requireBarkeeper(locals.user);
|
|
const data = await request.formData();
|
|
try {
|
|
deleteShare(actor, text(data, 'shareId'), text(data, 'showId'));
|
|
return { intent: 'deleteShare', success: 'Freigabe gelöscht.' };
|
|
} catch (cause) {
|
|
const problem = actionError(cause);
|
|
return fail(problem.status, { intent: 'deleteShare', ...problem });
|
|
}
|
|
},
|
|
start: async ({ locals, request }) => {
|
|
requireUser(locals.user);
|
|
const data = await request.formData();
|
|
return attempt('start', () =>
|
|
startOrResumeLive(text(data, 'showId'), text(data, 'commandId'), Number(data.get('revision')))
|
|
);
|
|
},
|
|
pause: async ({ locals, request }) => {
|
|
requireUser(locals.user);
|
|
const data = await request.formData();
|
|
return attempt('pause', () =>
|
|
pauseLive(text(data, 'showId'), text(data, 'commandId'), Number(data.get('revision')))
|
|
);
|
|
},
|
|
resume: async ({ locals, request }) => {
|
|
requireUser(locals.user);
|
|
const data = await request.formData();
|
|
return attempt('resume', () =>
|
|
resumeLive(text(data, 'showId'), text(data, 'commandId'), Number(data.get('revision')))
|
|
);
|
|
},
|
|
resetSectionTimer: async ({ locals, request }) => {
|
|
requireUser(locals.user);
|
|
const data = await request.formData();
|
|
return attempt('resetSectionTimer', () =>
|
|
resetSectionTimer(text(data, 'showId'), text(data, 'commandId'), Number(data.get('revision')))
|
|
);
|
|
},
|
|
resetTotalTimer: async ({ locals, request }) => {
|
|
requireUser(locals.user);
|
|
const data = await request.formData();
|
|
return attempt('resetTotalTimer', () =>
|
|
resetTotalTimer(text(data, 'showId'), text(data, 'commandId'), Number(data.get('revision')))
|
|
);
|
|
},
|
|
setStatus: async ({ locals, request }) => {
|
|
requireUser(locals.user);
|
|
const data = await request.formData();
|
|
const status = text(data, 'status');
|
|
if (status !== 'draft' && status !== 'ready') {
|
|
return fail(400, { intent: 'setStatus', error: 'Der Sendungsstatus ist ungültig.' });
|
|
}
|
|
return attempt('setStatus', () =>
|
|
setShowStatus(
|
|
text(data, 'showId'),
|
|
text(data, 'commandId'),
|
|
Number(data.get('revision')),
|
|
Number(data.get('showRevision')),
|
|
status
|
|
)
|
|
);
|
|
},
|
|
end: async ({ locals, request }) => {
|
|
requireUser(locals.user);
|
|
const data = await request.formData();
|
|
return attempt('end', () =>
|
|
endLive(text(data, 'showId'), text(data, 'commandId'), Number(data.get('revision')))
|
|
);
|
|
},
|
|
finalize: async ({ locals, request }) => {
|
|
const actor = requireUser(locals.user);
|
|
const data = await request.formData();
|
|
return attempt('finalize', () =>
|
|
finalizeShow({
|
|
showId: text(data, 'showId'),
|
|
commandId: text(data, 'commandId'),
|
|
expectedShowRevision: Number(data.get('showRevision')),
|
|
expectedLiveRevision: Number(data.get('revision')),
|
|
archivePieceIds: data.getAll('archivePieceIds').map(String),
|
|
actor
|
|
})
|
|
);
|
|
},
|
|
resumeEnded: async ({ locals, request }) => {
|
|
requireUser(locals.user);
|
|
const data = await request.formData();
|
|
return attempt('resumeEnded', () =>
|
|
resumeEndedLive(text(data, 'showId'), text(data, 'commandId'), Number(data.get('revision')))
|
|
);
|
|
},
|
|
startNewRun: async ({ locals, request }) => {
|
|
requireUser(locals.user);
|
|
const data = await request.formData();
|
|
return attempt('startNewRun', () =>
|
|
startNewLiveRun(text(data, 'showId'), text(data, 'commandId'), Number(data.get('revision')))
|
|
);
|
|
},
|
|
complete: async ({ locals, request }) => {
|
|
requireUser(locals.user);
|
|
const data = await request.formData();
|
|
return attempt('complete', () =>
|
|
completeCurrent(text(data, 'showId'), text(data, 'commandId'), Number(data.get('revision')))
|
|
);
|
|
},
|
|
skip: async ({ locals, request }) => {
|
|
requireUser(locals.user);
|
|
const data = await request.formData();
|
|
return attempt('skip', () =>
|
|
skipCurrent(text(data, 'showId'), text(data, 'commandId'), Number(data.get('revision')))
|
|
);
|
|
},
|
|
back: async ({ locals, request }) => {
|
|
requireUser(locals.user);
|
|
const data = await request.formData();
|
|
return attempt('back', () =>
|
|
goBack(text(data, 'showId'), text(data, 'commandId'), Number(data.get('revision')))
|
|
);
|
|
},
|
|
select: async ({ locals, request }) => {
|
|
requireUser(locals.user);
|
|
const data = await request.formData();
|
|
return attempt('select', () =>
|
|
selectUsage(
|
|
text(data, 'showId'),
|
|
text(data, 'usageId'),
|
|
text(data, 'commandId'),
|
|
Number(data.get('revision'))
|
|
)
|
|
);
|
|
},
|
|
present: async ({ locals, request }) => {
|
|
requireUser(locals.user);
|
|
const data = await request.formData();
|
|
return attempt('present', () =>
|
|
presentUsage(
|
|
text(data, 'showId'),
|
|
text(data, 'usageId') || null,
|
|
text(data, 'commandId'),
|
|
Number(data.get('revision'))
|
|
)
|
|
);
|
|
},
|
|
moveUsage: async ({ locals, request }) => {
|
|
requireUser(locals.user);
|
|
const data = await request.formData();
|
|
return attempt('moveUsage', () =>
|
|
moveOpenUsage(text(data, 'showId'), text(data, 'usageId'), Number(data.get('showRevision')), {
|
|
sectionId: text(data, 'sectionId') || null,
|
|
position: Number(data.get('position'))
|
|
})
|
|
);
|
|
},
|
|
createOutput: async ({ locals, request }) => {
|
|
requireUser(locals.user);
|
|
const data = await request.formData();
|
|
try {
|
|
const token = issuePresentationAccess(text(data, 'showId'));
|
|
return { intent: 'createOutput', success: true, outputUrl: `/heinzlive/default#${token}` };
|
|
} catch (cause) {
|
|
const problem = actionError(cause);
|
|
return fail(problem.status, { intent: 'createOutput', ...problem });
|
|
}
|
|
},
|
|
rotateOutput: async ({ locals, request }) => {
|
|
requireUser(locals.user);
|
|
const data = await request.formData();
|
|
try {
|
|
const token = rotatePresentationChannelKey(text(data, 'showId'));
|
|
return { intent: 'rotateOutput', success: true, outputUrl: `/heinzlive/default#${token}` };
|
|
} catch (cause) {
|
|
const problem = actionError(cause);
|
|
return fail(problem.status, { intent: 'rotateOutput', ...problem });
|
|
}
|
|
},
|
|
clearOutput: async ({ locals }) => {
|
|
requireUser(locals.user);
|
|
return attempt('clearOutput', () => clearPresentationChannel());
|
|
}
|
|
} satisfies Actions;
|