Procedurally generates a radio weather report
1import { OpenWeatherAPI, type DailyWeather } from 'openweather-api-node';
2import { voiceLines } from './voice.js';
3import type { Config } from './index.js';
4import type { Voice } from './voice.js';
5import crypto from 'crypto';
6
7type SegmentName = string;
8type SequenceName = string;
9type Programs = SegmentName[][];
10type Segments = { [segment: SegmentName]: SequenceName[] };
11type Sequence = {
12 conditions?: string[];
13 tracks: string[];
14}
15type Sequences = { [sequence: SequenceName]: Sequence };
16
17let config: Config = null;
18
19function selectOne<T>(arr: T[]): T {
20 return arr[crypto.randomInt(0, arr.length)];
21}
22
23function resolveSide(side: string, currentWeather: DailyWeather) {
24 if (!side.startsWith('weather')) {
25 return side.includes('.') ? parseFloat(side) : parseInt(side);
26 }
27
28 const tokens = side.split('.');
29 let w = currentWeather;
30 tokens.forEach(t => w = w[t]);
31 return typeof w === 'object' ? JSON.stringify(w) : w as (string | number);
32}
33
34function notNotANumber(something: number | string, defaultVal: string) {
35 if (typeof something === 'string' || !isNaN(something)) {
36 return something;
37 }
38 return defaultVal;
39}
40
41function conditionIsMet(condition: string | undefined, currentWeather: DailyWeather): boolean {
42 if (typeof condition !== 'string') {
43 return true;
44 }
45 const [lhs, relational, rhs] = condition.split(' ');
46 if (lhs === undefined || relational === undefined || rhs === undefined) {
47 throw new Error(`Condition "${condition}" is not in the correct format`);
48 }
49 const lhsResolved = notNotANumber(resolveSide(lhs, currentWeather), lhs);
50 const rhsResolved = notNotANumber(resolveSide(rhs, currentWeather), rhs);
51 switch (relational) {
52 case '=':
53 case '==':
54 return lhsResolved == rhsResolved;
55 case '!=':
56 return lhsResolved != rhsResolved;
57 case '<':
58 return lhsResolved < rhsResolved;
59 case '<=':
60 return lhsResolved <= rhsResolved;
61 case '>':
62 return lhsResolved > rhsResolved;
63 case '>=':
64 return lhsResolved >= rhsResolved;
65 default:
66 throw new Error(`Unsupported relational operator: ${relational}`);
67 }
68}
69
70function resolveMacro(str: string, currentWeather: DailyWeather): string[] {
71 if (str.startsWith('%')) {
72 const [profile, subject] = str.substring(1).split(' ', 2);
73 const voiceProfile: Voice = config.voices[profile];
74 let resolvedSubject: any = currentWeather;
75 subject.split('.').forEach(t => resolvedSubject = resolvedSubject[t]);
76 return voiceLines(voiceProfile, resolvedSubject);
77 }
78 return [str];
79}
80
81function processSequence(sequence: Sequence, currentWeather: DailyWeather): string[] {
82 const tracks = sequence.tracks;
83 return tracks.map(t => resolveMacro(t, currentWeather)).flat().filter(t => t !== null);
84}
85
86function processSegment(segment: SegmentName, currentWeather: DailyWeather): string[] {
87 if (!(segment in config.segments)) {
88 return (config.sequences[segment].conditions || []).every(c => conditionIsMet(c, currentWeather)) ? processSequence(config.sequences[segment], currentWeather) : [];
89 }
90 const potentialSequences: SequenceName[] = config.segments[segment].filter(s => (config.sequences[s].conditions || []).every(c => conditionIsMet(c, currentWeather)));
91 if (potentialSequences.length === 0) {
92 return [];
93 }
94 return processSequence(config.sequences[selectOne(potentialSequences)], currentWeather);
95}
96
97async function Sequencer(conf: Config): Promise<string[]> {
98 config = conf;
99 const weather = new OpenWeatherAPI(conf.weather);
100 const currentWeather = await weather.getToday();
101 const sequence: string[] = [];
102 const program: SegmentName[] = selectOne(conf.programs);
103 for (let i = 0; i < program.length; i++) {
104 const segment = program[i];
105 sequence.push(...(processSegment(segment, currentWeather)));
106 }
107 return sequence;
108}
109
110export default Sequencer;
111export { Sequencer };
112export type { SegmentName, SequenceName, Programs, Segments, Sequence, Sequences };