music on hold: play order set in the UI, and shuffle

Owner: the first upload always played first. A class is now an Asterisk playlist in the order
set on Sounds & music (move up/down), new uploads last, with a Shuffle option (sort = random).
moh-test.ts plays both in a real Asterisk and reads the file order from its debug log.
This commit is contained in:
iamdoubz committed 2026-10-21 13:27:02 -05:00
1 parent aa6681da03
commit 175d5a2c73
17 files changed
+4432 -27

No files matched your search

+14 -4
View File
@@ -200,7 +200,7 @@ export type Snapshot = {
* Music-on-hold classes that have music ('default' is built in and not here). An empty class
* would play silence; left out, Asterisk falls back to default (seen in M8-T5).
*/
mohClasses: string[];
mohClasses: SnapMohClass[];
/** Played before recording a call that a phone number records (Settings → Recording). */
recordingAnnouncement: string | null;
/** Call audio ports from the environment (RTP_PORT_START/END), not the database. */
@@ -209,6 +209,9 @@ export type Snapshot = {
export const DEFAULT_RTP_PORTS = { start: 10000, end: 20000 };
/** A music-on-hold class: its files (sound_files ids, <MOH_DIR>/<name>/<id>.wav) in play order. */
export type SnapMohClass = { name: string; shuffle: boolean; files: string[] };
export function todayIn(timezone: string, now = new Date()): string {
// en-CA formats as YYYY-MM-DD.
return new Intl.DateTimeFormat('en-CA', { timeZone: timezone }).format(now);
@@ -265,9 +268,16 @@ export async function loadSnapshot(db: Db, now = new Date()): Promise<Snapshot>
const holidayRows = await db.select().from(holidays);
const annRows = await db.select().from(announcements);
const mohRows = await db
.selectDistinct({ name: mohClasses.name })
.select({ name: mohClasses.name, shuffle: mohClasses.shuffle, file: soundFiles.id })
.from(mohClasses)
.innerJoin(soundFiles, eq(soundFiles.mohClassId, mohClasses.id));
.innerJoin(soundFiles, eq(soundFiles.mohClassId, mohClasses.id))
.orderBy(asc(mohClasses.name), asc(soundFiles.position), asc(soundFiles.createdAt));
const moh = new Map<string, SnapMohClass>();
for (const r of mohRows) {
const c = moh.get(r.name) ?? { name: r.name, shuffle: r.shuffle, files: [] };
c.files.push(r.file);
moh.set(r.name, c);
}
const optionRows = await db.select().from(ivrOptions);
const soundName = new Map(
(await db.select({ id: soundFiles.id, name: soundFiles.asteriskName }).from(soundFiles)).map(
@@ -366,7 +376,7 @@ export async function loadSnapshot(db: Db, now = new Date()): Promise<Snapshot>
maxMembers: c.maxMembers,
record: c.record,
})),
mohClasses: mohRows.map((m) => m.name),
mohClasses: [...moh.values()],
pagingGroups: pageRows.map((g) => ({
id: g.id,
number: g.number,