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