import { OpenWeatherAPI, type DailyWeather } from 'openweather-api-node'; import { voiceLines } from './voice.js'; import type { Config } from './index.js'; import type { Voice } from './voice.js'; import crypto from 'crypto'; type SegmentName = string; type SequenceName = string; type Programs = SegmentName[][]; type Segments = { [segment: SegmentName]: SequenceName[] }; type Sequence = { conditions?: string[]; tracks: string[]; } type Sequences = { [sequence: SequenceName]: Sequence }; let config: Config = null; function selectOne(arr: T[]): T { return arr[crypto.randomInt(0, arr.length)]; } function resolveSide(side: string, currentWeather: DailyWeather) { if (!side.startsWith('weather')) { return side.includes('.') ? parseFloat(side) : parseInt(side); } const tokens = side.split('.'); let w = currentWeather; tokens.forEach(t => w = w[t]); return typeof w === 'object' ? JSON.stringify(w) : w as (string | number); } function notNotANumber(something: number | string, defaultVal: string) { if (typeof something === 'string' || !isNaN(something)) { return something; } return defaultVal; } function conditionIsMet(condition: string | undefined, currentWeather: DailyWeather): boolean { if (typeof condition !== 'string') { return true; } const [lhs, relational, rhs] = condition.split(' '); if (lhs === undefined || relational === undefined || rhs === undefined) { throw new Error(`Condition "${condition}" is not in the correct format`); } const lhsResolved = notNotANumber(resolveSide(lhs, currentWeather), lhs); const rhsResolved = notNotANumber(resolveSide(rhs, currentWeather), rhs); switch (relational) { case '=': case '==': return lhsResolved == rhsResolved; case '!=': return lhsResolved != rhsResolved; case '<': return lhsResolved < rhsResolved; case '<=': return lhsResolved <= rhsResolved; case '>': return lhsResolved > rhsResolved; case '>=': return lhsResolved >= rhsResolved; default: throw new Error(`Unsupported relational operator: ${relational}`); } } function resolveMacro(str: string, currentWeather: DailyWeather): string[] { if (str.startsWith('%')) { const [profile, subject] = str.substring(1).split(' ', 2); const voiceProfile: Voice = config.voices[profile]; let resolvedSubject: any = currentWeather; subject.split('.').forEach(t => resolvedSubject = resolvedSubject[t]); return voiceLines(voiceProfile, resolvedSubject); } return [str]; } function processSequence(sequence: Sequence, currentWeather: DailyWeather): string[] { const tracks = sequence.tracks; return tracks.map(t => resolveMacro(t, currentWeather)).flat().filter(t => t !== null); } function processSegment(segment: SegmentName, currentWeather: DailyWeather): string[] { if (!(segment in config.segments)) { return (config.sequences[segment].conditions || []).every(c => conditionIsMet(c, currentWeather)) ? processSequence(config.sequences[segment], currentWeather) : []; } const potentialSequences: SequenceName[] = config.segments[segment].filter(s => (config.sequences[s].conditions || []).every(c => conditionIsMet(c, currentWeather))); if (potentialSequences.length === 0) { return []; } return processSequence(config.sequences[selectOne(potentialSequences)], currentWeather); } async function Sequencer(conf: Config): Promise { config = conf; const weather = new OpenWeatherAPI(conf.weather); const currentWeather = await weather.getToday(); const sequence: string[] = []; const program: SegmentName[] = selectOne(conf.programs); for (let i = 0; i < program.length; i++) { const segment = program[i]; sequence.push(...(processSegment(segment, currentWeather))); } return sequence; } export default Sequencer; export { Sequencer }; export type { SegmentName, SequenceName, Programs, Segments, Sequence, Sequences };