+1
-1
package.json
+1
-1
package.json
+19
-13
src/sequencer.ts
+19
-13
src/sequencer.ts
···
1
-
import { OpenWeatherAPI, type CurrentWeather } from 'openweather-api-node';
2
import { voiceLines } from './voice.js';
3
import type { Config } from './index.js';
4
import type { Voice } from './voice.js';
···
19
return arr[Math.floor(Math.random() * arr.length)];
20
}
21
22
-
function resolveSide(side: string, currentWeather: CurrentWeather) {
23
if (!side.startsWith('weather')) {
24
return side.includes('.') ? parseFloat(side) : parseInt(side);
25
}
···
30
return typeof w === 'object' ? JSON.stringify(w) : w as (string | number);
31
}
32
33
-
function conditionIsMet(condition: string | undefined, currentWeather: CurrentWeather): boolean {
34
if (typeof condition !== 'string') {
35
return true;
36
}
37
const [lhs, relational, rhs] = condition.split(' ');
38
-
const lhsResolved = resolveSide(lhs, currentWeather);
39
-
const rhsResolved = resolveSide(rhs, currentWeather);
40
switch (relational) {
41
case '=':
42
case '==':
···
56
}
57
}
58
59
-
function resolveMacro(str: string, currentWeather: CurrentWeather): string[] {
60
if (str.startsWith('%')) {
61
const [profile, subject] = str.substring(1).split(' ', 2);
62
const voiceProfile: Voice = config.voices[profile];
···
64
subject.split('.').forEach(t => resolvedSubject = resolvedSubject[t]);
65
return voiceLines(voiceProfile, resolvedSubject);
66
}
67
-
else if (str.startsWith('$')) {
68
-
return null;
69
-
}
70
return [str];
71
}
72
73
-
function processSequence(sequence: Sequence, currentWeather: CurrentWeather): string[] {
74
const tracks = sequence.tracks;
75
return tracks.map(t => resolveMacro(t, currentWeather)).flat().filter(t => t !== null);
76
}
77
78
-
function processSegment(segment: SegmentName, currentWeather: CurrentWeather): string[] {
79
if (!(segment in config.segments)) {
80
return (config.sequences[segment].conditions || []).every(c => conditionIsMet(c, currentWeather)) ? processSequence(config.sequences[segment], currentWeather) : [];
81
}
···
89
async function Sequencer(conf: Config): Promise<string[]> {
90
config = conf;
91
const weather = new OpenWeatherAPI(conf.weather);
92
-
const currentWeather = await weather.getCurrent();
93
-
console.log(JSON.stringify(currentWeather));
94
const sequence: string[] = [];
95
const program: SegmentName[] = selectOne(conf.programs);
96
for (let i = 0; i < program.length; i++) {
···
1
+
import { OpenWeatherAPI, type DailyWeather } from 'openweather-api-node';
2
import { voiceLines } from './voice.js';
3
import type { Config } from './index.js';
4
import type { Voice } from './voice.js';
···
19
return arr[Math.floor(Math.random() * arr.length)];
20
}
21
22
+
function resolveSide(side: string, currentWeather: DailyWeather) {
23
if (!side.startsWith('weather')) {
24
return side.includes('.') ? parseFloat(side) : parseInt(side);
25
}
···
30
return typeof w === 'object' ? JSON.stringify(w) : w as (string | number);
31
}
32
33
+
function notNotANumber(something: number | string, defaultVal: string) {
34
+
if (typeof something === 'string' || !isNaN(something)) {
35
+
return something;
36
+
}
37
+
return defaultVal;
38
+
}
39
+
40
+
function 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 '==':
···
66
}
67
}
68
69
+
function 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];
···
74
subject.split('.').forEach(t => resolvedSubject = resolvedSubject[t]);
75
return voiceLines(voiceProfile, resolvedSubject);
76
}
77
return [str];
78
}
79
80
+
function 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
85
+
function 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
}
···
96
async 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++) {
+81
-1
test/sequencer.test.ts
+81
-1
test/sequencer.test.ts
···
9
return {
10
OpenWeatherAPI: vi.fn().mockImplementation((_) => {
11
return {
12
+
getToday: vi.fn(() => {
13
return {
14
"lat": 43.0748,
15
"lon": -89.3838,
···
105
voices: {},
106
weather: dummyWeather
107
})).to.be.ordered.members(['seq1.flac', 'seq1.flac']);
108
+
});
109
+
110
+
it('throws an error on invalid conditions', async () => {
111
+
await expect(Sequencer({
112
+
programs: [['sequence 1']],
113
+
segments: {
114
+
},
115
+
sequences: {
116
+
'sequence 1': {
117
+
'conditions': ['100'],
118
+
'tracks': [
119
+
'seq1.flac'
120
+
]
121
+
}
122
+
},
123
+
voices: {},
124
+
weather: dummyWeather
125
+
})).rejects.toThrow(/not in the correct format/);
126
+
127
+
await expect(Sequencer({
128
+
programs: [['sequence 1']],
129
+
segments: {
130
+
},
131
+
sequences: {
132
+
'sequence 1': {
133
+
'conditions': ['1 ~ 2'],
134
+
'tracks': [
135
+
'seq1.flac'
136
+
]
137
+
}
138
+
},
139
+
voices: {},
140
+
weather: dummyWeather
141
+
})).rejects.toThrow(/Unsupported relational operator/);
142
+
});
143
+
144
+
it('can stringify conditions', async () => {
145
+
expect(await Sequencer({
146
+
programs: [['sequence 1']],
147
+
segments: {
148
+
},
149
+
sequences: {
150
+
'sequence 1': {
151
+
'conditions': ['weather.feelsLike = {"cur":55.31}'],
152
+
'tracks': [
153
+
'seq1.flac'
154
+
]
155
+
}
156
+
},
157
+
voices: {},
158
+
weather: dummyWeather
159
+
})).to.be.ordered.members(['seq1.flac']);
160
+
});
161
+
162
+
it('can parse voice macros', async () => {
163
+
expect(await Sequencer({
164
+
programs: [['sequence 1']],
165
+
segments: {
166
+
},
167
+
sequences: {
168
+
'sequence 1': {
169
+
'tracks': [
170
+
'%alice weather.temp.max'
171
+
]
172
+
}
173
+
},
174
+
voices: {
175
+
"alice": {
176
+
"directory": "alice/",
177
+
"extension": "flac"
178
+
}
179
+
},
180
+
weather: dummyWeather
181
+
})).to.be.ordered.members([
182
+
'alice/fifty.flac',
183
+
'alice/eight.flac',
184
+
'alice/point.flac',
185
+
'alice/zero.flac',
186
+
'alice/one.flac'
187
+
]);
188
});
189
});
190