6e94a0c998
- Admin TVs tab: wake time/days, YouTube Music playlist, Shield volume per TV, pushed via 5-min TV check-ins (/api/tv/config, /api/tv/report, LAN-only) - com.dayline.photos 1.2.1: MorningActivity (screen on, volume, playlist), calendar alarm at wake time, Doze-safe chained check-ins - Both TVs configured for 06:30 daily with the family playlist Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E9NnVEico3WvFxLyjy1iW3
73 lines
3.4 KiB
TypeScript
73 lines
3.4 KiB
TypeScript
import { json, readJsonBody, requireAdmin } from "../../../_lib/auth";
|
|
import { ALL_DAYS, MAX_TVS, playlistUrl, readSettings, saveSettings, type TvSetting, validPlaylistId, validTvId } from "../../../_lib/store";
|
|
import { CHECK_IN_MINUTES, readTvStatus, tvConfig } from "../../../_lib/tvs";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
async function view(tvs: TvSetting[]) {
|
|
const status = await readTvStatus();
|
|
const known = new Set(tvs.map((tv) => tv.id));
|
|
return {
|
|
checkInMinutes: CHECK_IN_MINUTES,
|
|
tvs: tvs.map((tv) => ({
|
|
...tv,
|
|
playlistUrl: tv.music.playlistId ? playlistUrl(tv.music.playlistId) : "",
|
|
version: tvConfig(tv).version,
|
|
status: status[tv.id] ?? null,
|
|
})),
|
|
// TVs running the photo app that have checked in but are not configured yet.
|
|
found: Object.values(status)
|
|
.filter((entry) => !known.has(entry.id))
|
|
.sort((left, right) => right.lastSeenAt - left.lastSeenAt),
|
|
};
|
|
}
|
|
|
|
export async function GET(request: Request) {
|
|
const denied = await requireAdmin(request);
|
|
if (denied) return denied;
|
|
return json(await view((await readSettings()).tvs));
|
|
}
|
|
|
|
type IncomingTv = {
|
|
id?: string;
|
|
name?: string;
|
|
wake?: { enabled?: boolean; time?: string; days?: number[] };
|
|
music?: { enabled?: boolean; playlist?: string };
|
|
volume?: { enabled?: boolean; level?: number };
|
|
};
|
|
|
|
export async function PUT(request: Request) {
|
|
const denied = await requireAdmin(request, { mutating: true });
|
|
if (denied) return denied;
|
|
const body = await readJsonBody<{ tvs?: IncomingTv[] }>(request);
|
|
if (!body || !Array.isArray(body.tvs)) return json({ error: "Expected a list of TVs." }, 400);
|
|
if (body.tvs.length > MAX_TVS) return json({ error: `At most ${MAX_TVS} TVs.` }, 400);
|
|
|
|
const tvs: TvSetting[] = [];
|
|
for (const [position, entry] of body.tvs.entries()) {
|
|
const label = entry.name?.trim() || `TV ${position + 1}`;
|
|
const id = validTvId(entry.id);
|
|
if (!id) return json({ error: `${label}: unknown TV id; reload the page.` }, 400);
|
|
const time = String(entry.wake?.time ?? "");
|
|
if (entry.wake?.enabled && !/^([01]\d|2[0-3]):[0-5]\d$/.test(time)) return json({ error: `${label}: pick a wake time.` }, 400);
|
|
const playlistText = String(entry.music?.playlist ?? "").trim();
|
|
const playlistId = playlistText ? validPlaylistId(playlistText) : "";
|
|
if (playlistText && !playlistId) return json({ error: `${label}: paste a YouTube Music playlist link (https://music.youtube.com/playlist?list=…).` }, 400);
|
|
if (entry.music?.enabled && !playlistId) return json({ error: `${label}: music is on but no playlist is set.` }, 400);
|
|
const days = Array.isArray(entry.wake?.days) ? entry.wake.days : ALL_DAYS;
|
|
if (entry.wake?.enabled && !days.length) return json({ error: `${label}: pick at least one day.` }, 400);
|
|
tvs.push({
|
|
id,
|
|
name: (entry.name ?? "").trim().slice(0, 40),
|
|
wake: { enabled: entry.wake?.enabled === true, time: time || "06:30", days },
|
|
music: { enabled: entry.music?.enabled === true, playlistId: playlistId || "" },
|
|
volume: { enabled: entry.volume?.enabled === true, level: Number(entry.volume?.level ?? 40) },
|
|
});
|
|
}
|
|
if (new Set(tvs.map((tv) => tv.id)).size !== tvs.length) return json({ error: "The same TV is listed twice." }, 400);
|
|
|
|
const saved = await saveSettings({ tvs }, `Saved TV settings (${tvs.length} TV(s))`);
|
|
console.log(`[admin] TV settings saved (${tvs.length} TV(s))`);
|
|
return json(await view(saved.tvs));
|
|
}
|