+27
.github/workflows/npm-audit.yml
+27
.github/workflows/npm-audit.yml
···
1
+
name: NPM Audit Check
2
+
3
+
on:
4
+
push:
5
+
branches: [ master ]
6
+
pull_request:
7
+
branches: [ master ]
8
+
schedule:
9
+
- cron: '15 16 * * 5'
10
+
11
+
jobs:
12
+
13
+
npm_audit:
14
+
name: Check NPM audit
15
+
runs-on: ubuntu-latest
16
+
timeout-minutes: 20
17
+
strategy:
18
+
fail-fast: true
19
+
permissions:
20
+
contents: read
21
+
22
+
steps:
23
+
- name: Checkout repository
24
+
uses: https://github.com/actions/checkout@v4
25
+
26
+
- name: NPM Audit
27
+
run: npm audit
+104
.gitignore
+104
.gitignore
···
1
+
# Logs
2
+
logs
3
+
*.log
4
+
npm-debug.log*
5
+
yarn-debug.log*
6
+
yarn-error.log*
7
+
lerna-debug.log*
8
+
9
+
# Diagnostic reports (https://nodejs.org/api/report.html)
10
+
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11
+
12
+
# Runtime data
13
+
pids
14
+
*.pid
15
+
*.seed
16
+
*.pid.lock
17
+
18
+
# Directory for instrumented libs generated by jscoverage/JSCover
19
+
lib-cov
20
+
21
+
# Coverage directory used by tools like istanbul
22
+
coverage
23
+
*.lcov
24
+
25
+
# nyc test coverage
26
+
.nyc_output
27
+
28
+
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29
+
.grunt
30
+
31
+
# Bower dependency directory (https://bower.io/)
32
+
bower_components
33
+
34
+
# node-waf configuration
35
+
.lock-wscript
36
+
37
+
# Compiled binary addons (https://nodejs.org/api/addons.html)
38
+
build/Release
39
+
40
+
# Dependency directories
41
+
node_modules/
42
+
jspm_packages/
43
+
44
+
# TypeScript v1 declaration files
45
+
typings/
46
+
47
+
# TypeScript cache
48
+
*.tsbuildinfo
49
+
50
+
# Optional npm cache directory
51
+
.npm
52
+
53
+
# Optional eslint cache
54
+
.eslintcache
55
+
56
+
# Microbundle cache
57
+
.rpt2_cache/
58
+
.rts2_cache_cjs/
59
+
.rts2_cache_es/
60
+
.rts2_cache_umd/
61
+
62
+
# Optional REPL history
63
+
.node_repl_history
64
+
65
+
# Output of 'npm pack'
66
+
*.tgz
67
+
68
+
# Yarn Integrity file
69
+
.yarn-integrity
70
+
71
+
# dotenv environment variables file
72
+
.env
73
+
.env.test
74
+
75
+
# parcel-bundler cache (https://parceljs.org/)
76
+
.cache
77
+
78
+
# next.js build output
79
+
.next
80
+
81
+
# nuxt.js build output
82
+
.nuxt
83
+
84
+
# gatsby files
85
+
.cache/
86
+
public
87
+
88
+
# vuepress build output
89
+
.vuepress/dist
90
+
91
+
# Serverless directories
92
+
.serverless/
93
+
94
+
# FuseBox cache
95
+
.fusebox/
96
+
97
+
# DynamoDB Local files
98
+
.dynamodb/
99
+
100
+
101
+
# custom .gitignore
102
+
config/config.json5
103
+
distribution/
104
+
.env
+12
README.md
+12
README.md
+118
config/config.example.json5
+118
config/config.example.json5
···
1
+
{
2
+
"programs": [
3
+
[
4
+
"intro music",
5
+
"intro",
6
+
"hi's and low's",
7
+
"notable weather",
8
+
"signoff"
9
+
]
10
+
],
11
+
"segments": {
12
+
"intro": [
13
+
"intro 1",
14
+
"intro 2"
15
+
],
16
+
"hi's and low's": [
17
+
"hilo 1",
18
+
"hilo 2"
19
+
],
20
+
"notable weather": [
21
+
"rain 1",
22
+
"rain 2",
23
+
"storm 1",
24
+
"storm 2",
25
+
"snow 1",
26
+
"hail 1"
27
+
],
28
+
"signoff": [
29
+
"signoff 1",
30
+
"signoff 2"
31
+
]
32
+
},
33
+
"sequences": {
34
+
"intro music": {
35
+
"tracks": [
36
+
"audio/intro_music.flac"
37
+
]
38
+
},
39
+
"intro 1": {
40
+
"tracks": [
41
+
"audio/intro_01.flac"
42
+
]
43
+
},
44
+
"intro 2": {
45
+
"tracks": [
46
+
"audio/intro_02.flac"
47
+
]
48
+
},
49
+
"hilo 1": {
50
+
"tracks": [
51
+
"audio/hi_01.flac",
52
+
"%cory hi",
53
+
"audio/lo_01.flac",
54
+
"%cory lo"
55
+
]
56
+
},
57
+
"hilo 2": {
58
+
"tracks": [
59
+
"audio/hi_02.flac",
60
+
"%cory hi",
61
+
"audio/lo_02.flac",
62
+
"%cory lo"
63
+
]
64
+
},
65
+
"rain 1": {
66
+
"condition": "weather == rain",
67
+
"tracks": [
68
+
"audio/rain1.flac"
69
+
]
70
+
},
71
+
"rain 2": {
72
+
"condition": "weather == rain",
73
+
"tracks": [
74
+
"audio/rain2.flac"
75
+
]
76
+
},
77
+
"storm 1": {
78
+
"condition": "weather == storm",
79
+
"tracks": [
80
+
"audio/storm1.flac"
81
+
]
82
+
},
83
+
"storm 2": {
84
+
"condition": "weather == storm",
85
+
"tracks": [
86
+
"audio/storm2.flac"
87
+
]
88
+
},
89
+
"snow 1": {
90
+
"condition": "weather == snow",
91
+
"tracks": [
92
+
"audio/snow1.flac"
93
+
]
94
+
},
95
+
"hail 1": {
96
+
"condition": "weather == hail",
97
+
"tracks": [
98
+
"audio/hail1.flac"
99
+
]
100
+
},
101
+
"signoff 1": {
102
+
"tracks": [
103
+
"audio/signoff_01.flac"
104
+
]
105
+
},
106
+
"signoff 2": {
107
+
"tracks": [
108
+
"audio/signoff_02.flac"
109
+
]
110
+
}
111
+
},
112
+
"voices": {
113
+
"cory": {
114
+
"directory": "audio/voice/cory/",
115
+
"extension": "flac"
116
+
}
117
+
}
118
+
}
+115
package-lock.json
+115
package-lock.json
···
1
+
{
2
+
"name": "morning-report",
3
+
"version": "0.0.1",
4
+
"lockfileVersion": 3,
5
+
"requires": true,
6
+
"packages": {
7
+
"": {
8
+
"name": "morning-report",
9
+
"version": "0.0.1",
10
+
"license": "MIT",
11
+
"dependencies": {
12
+
"json5": "2.2.3",
13
+
"openweathermap-ts": "1.2.10"
14
+
},
15
+
"devDependencies": {
16
+
"@types/node": "24.3.0",
17
+
"typescript": "5.9.2"
18
+
}
19
+
},
20
+
"node_modules/@types/node": {
21
+
"version": "24.3.0",
22
+
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz",
23
+
"integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==",
24
+
"dev": true,
25
+
"license": "MIT",
26
+
"dependencies": {
27
+
"undici-types": "~7.10.0"
28
+
}
29
+
},
30
+
"node_modules/json5": {
31
+
"version": "2.2.3",
32
+
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
33
+
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
34
+
"license": "MIT",
35
+
"bin": {
36
+
"json5": "lib/cli.js"
37
+
},
38
+
"engines": {
39
+
"node": ">=6"
40
+
}
41
+
},
42
+
"node_modules/node-fetch": {
43
+
"version": "2.7.0",
44
+
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
45
+
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
46
+
"license": "MIT",
47
+
"dependencies": {
48
+
"whatwg-url": "^5.0.0"
49
+
},
50
+
"engines": {
51
+
"node": "4.x || >=6.0.0"
52
+
},
53
+
"peerDependencies": {
54
+
"encoding": "^0.1.0"
55
+
},
56
+
"peerDependenciesMeta": {
57
+
"encoding": {
58
+
"optional": true
59
+
}
60
+
}
61
+
},
62
+
"node_modules/openweathermap-ts": {
63
+
"version": "1.2.10",
64
+
"resolved": "https://registry.npmjs.org/openweathermap-ts/-/openweathermap-ts-1.2.10.tgz",
65
+
"integrity": "sha512-Zckv2aXN8ENSeAeroces2jJciLWb6aLNXEmvG6pmF+BcIMw2kwRo6++/AKUNoU5suOp47UWA6lllDV0TNm//OA==",
66
+
"license": "MIT",
67
+
"dependencies": {
68
+
"node-fetch": "^2.6.0"
69
+
}
70
+
},
71
+
"node_modules/tr46": {
72
+
"version": "0.0.3",
73
+
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
74
+
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
75
+
"license": "MIT"
76
+
},
77
+
"node_modules/typescript": {
78
+
"version": "5.9.2",
79
+
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
80
+
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
81
+
"dev": true,
82
+
"license": "Apache-2.0",
83
+
"bin": {
84
+
"tsc": "bin/tsc",
85
+
"tsserver": "bin/tsserver"
86
+
},
87
+
"engines": {
88
+
"node": ">=14.17"
89
+
}
90
+
},
91
+
"node_modules/undici-types": {
92
+
"version": "7.10.0",
93
+
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
94
+
"integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
95
+
"dev": true,
96
+
"license": "MIT"
97
+
},
98
+
"node_modules/webidl-conversions": {
99
+
"version": "3.0.1",
100
+
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
101
+
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
102
+
"license": "BSD-2-Clause"
103
+
},
104
+
"node_modules/whatwg-url": {
105
+
"version": "5.0.0",
106
+
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
107
+
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
108
+
"license": "MIT",
109
+
"dependencies": {
110
+
"tr46": "~0.0.3",
111
+
"webidl-conversions": "^3.0.0"
112
+
}
113
+
}
114
+
}
115
+
}
+34
package.json
+34
package.json
···
1
+
{
2
+
"name": "morning-report",
3
+
"version": "0.0.1",
4
+
"description": "Procedurally generates a radio weather report",
5
+
"keywords": [
6
+
"weather",
7
+
"radio",
8
+
"audio"
9
+
],
10
+
"repository": {
11
+
"type": "git",
12
+
"url": "ssh://git@git.sanin.dev:3689/corysanin/morning-report.git"
13
+
},
14
+
"license": "MIT",
15
+
"author": {
16
+
"name": "Cory Sanin",
17
+
"email": "corysanin@outlook.com",
18
+
"url": "https://sanin.dev/"
19
+
},
20
+
"type": "module",
21
+
"main": "distribution/index.js",
22
+
"scripts": {
23
+
"build": "npx tsc",
24
+
"start": "node distribution/index.js"
25
+
},
26
+
"dependencies": {
27
+
"json5": "2.2.3",
28
+
"openweathermap-ts": "1.2.10"
29
+
},
30
+
"devDependencies": {
31
+
"typescript": "5.9.2",
32
+
"@types/node": "24.3.0"
33
+
}
34
+
}
+23
src/index.ts
+23
src/index.ts
···
1
+
import path from 'path';
2
+
import fsp from 'fs/promises';
3
+
import json5 from 'json5';
4
+
import Sequencer from './sequencer.js';
5
+
import type {Programs, Segments, Sequences} from './sequencer.js';
6
+
import type { Voices } from './voice.js';
7
+
8
+
9
+
interface Config {
10
+
programs: Programs,
11
+
segments: Segments,
12
+
sequences: Sequences,
13
+
voices: Voices
14
+
}
15
+
16
+
console.log('morning-report\nCory Sanin 2025\n');
17
+
18
+
const config: Config = json5.parse(await fsp.readFile(process.env['CONFIG'] || path.join('config', 'config.json5'), { encoding: 'utf-8' }));
19
+
const sequence = Sequencer(config);
20
+
console.log(sequence.join('\n'));
21
+
22
+
23
+
export type { Config };
+54
src/sequencer.ts
+54
src/sequencer.ts
···
1
+
import type { Config } from './index.js';
2
+
3
+
type SegmentName = string;
4
+
type SequenceName = string;
5
+
type Programs = SegmentName[][];
6
+
type Segments = { [segment: SegmentName]: SequenceName[] };
7
+
type Sequence = {
8
+
condition?: string;
9
+
tracks: string[];
10
+
}
11
+
type Sequences = { [sequence: SequenceName]: Sequence };
12
+
13
+
let config: Config = null;
14
+
15
+
function selectOne<T>(arr: T[]): T {
16
+
return arr[Math.floor(Math.random() * arr.length)];
17
+
}
18
+
19
+
function conditionIsMet(condition: string | undefined = undefined): boolean {
20
+
if (typeof condition !== 'string') {
21
+
return true;
22
+
}
23
+
// TODO: parse condition, return bool
24
+
return false;
25
+
}
26
+
27
+
function processSequence(sequence: Sequence): string[] {
28
+
const tracks = sequence.tracks;
29
+
// TODO: process voice macros
30
+
return tracks;
31
+
}
32
+
33
+
function processSegment(segment: SegmentName): string[] {
34
+
if (!(segment in config.segments)) {
35
+
return processSequence(config.sequences[segment]);
36
+
}
37
+
const potentialSequences: SequenceName[] = config.segments[segment].filter(s => conditionIsMet(config.sequences[s].condition));
38
+
if (potentialSequences.length === 0) {
39
+
return [];
40
+
}
41
+
return processSequence(config.sequences[selectOne(potentialSequences)]);
42
+
}
43
+
44
+
function Sequencer(conf: Config): string[] {
45
+
config = conf;
46
+
const sequence: string[] = [];
47
+
const program: SegmentName[] = selectOne(conf.programs);
48
+
program.forEach(segment => sequence.push(...processSegment(segment)));
49
+
return sequence;
50
+
}
51
+
52
+
export default Sequencer;
53
+
export { Sequencer };
54
+
export type { SegmentName, SequenceName, Programs, Segments, Sequence, Sequences };
src/stitcher.ts
src/stitcher.ts
This is a binary file and will not be displayed.
+10
src/voice.ts
+10
src/voice.ts
src/weather.ts
src/weather.ts
This is a binary file and will not be displayed.
+17
tsconfig.json
+17
tsconfig.json
···
1
+
{
2
+
"compilerOptions": {
3
+
"lib": ["ESNext"],
4
+
"module": "NodeNext",
5
+
"target": "ESNext",
6
+
7
+
"esModuleInterop": true,
8
+
"skipLibCheck": true,
9
+
"moduleResolution": "node16",
10
+
"sourceMap": true,
11
+
"inlineSources": true,
12
+
"rootDir": "./src",
13
+
"outDir": "./distribution"
14
+
},
15
+
"include": ["src/**/*"],
16
+
"exclude": ["**/*.spec.ts"]
17
+
}