62 lines
3.2 KiB
TypeScript
62 lines
3.2 KiB
TypeScript
import { readFileSync } from 'node:fs';
|
||
import type { WeatherSample } from '../../src/weather/weather.ts';
|
||
import { kstClock, kstFields } from '../../src/ui/time.ts';
|
||
import { THREADS } from '../../src/constants/threads.ts';
|
||
|
||
type Condition = typeof THREADS.captionConditions[number];
|
||
export interface Captions {
|
||
template: string[];
|
||
testPrefix: string;
|
||
thresholds: { fogVisibilityMeters: number; cloudyPercent: number; partlyCloudyPercent: number };
|
||
messages: Record<Condition, string[]>;
|
||
}
|
||
|
||
export function loadCaptions(path: string | URL = new URL('../../.threads-captions.json', import.meta.url)): Captions {
|
||
let text: string;
|
||
try { text = readFileSync(path, 'utf8'); }
|
||
catch (error) {
|
||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
||
text = readFileSync(new URL('../../.threads-captions.sample.json', import.meta.url), 'utf8');
|
||
}
|
||
let config: Captions;
|
||
try { config = JSON.parse(text); } catch { throw new Error('Invalid JSON in .threads-captions.json'); }
|
||
const lines = (value: unknown): value is string[] => Array.isArray(value) && value.length > 0 && value.every(line => typeof line === 'string');
|
||
if (!config || !lines(config.template) || typeof config.testPrefix !== 'string'
|
||
|| !THREADS.captionConditions.every(key => lines(config.messages?.[key]))) throw new Error('Invalid caption template or weather messages');
|
||
const t = config.thresholds;
|
||
if (!t || !Number.isFinite(t.fogVisibilityMeters) || t.fogVisibilityMeters < 0
|
||
|| !Number.isFinite(t.partlyCloudyPercent) || !Number.isFinite(t.cloudyPercent)
|
||
|| t.partlyCloudyPercent < 0 || t.cloudyPercent > 100 || t.partlyCloudyPercent >= t.cloudyPercent) {
|
||
throw new Error('Invalid caption weather thresholds');
|
||
}
|
||
return config;
|
||
}
|
||
|
||
export function weatherKey(weather: WeatherSample | null, config: Captions): Condition {
|
||
if (!weather) return 'unknown';
|
||
if ((weather.snowfall ?? 0) > 0) return 'snow';
|
||
if ((weather.rain ?? 0) > 0) return 'rain';
|
||
if (weather.visibility !== null && weather.visibility < config.thresholds.fogVisibilityMeters) return 'fog';
|
||
if (weather.rain === null || weather.snowfall === null || !Number.isFinite(weather.cloudCover)) return 'unknown';
|
||
if (weather.cloudCover >= config.thresholds.cloudyPercent) return 'cloudy';
|
||
if (weather.cloudCover >= config.thresholds.partlyCloudyPercent) return 'partlyCloudy';
|
||
return 'clear';
|
||
}
|
||
|
||
export function renderCaption(config: Captions, capture: {
|
||
weather: WeatherSample | null; capturedAt: string; sunrise: string; url: string; test: boolean;
|
||
}) {
|
||
const condition = weatherKey(capture.weather, config);
|
||
const date = new Date(capture.capturedAt);
|
||
const values: Record<string, string> = {
|
||
message: config.messages[condition].join('\n'), date: kstFields(date).date,
|
||
time: kstClock(date).slice(0, 5), sunrise: kstClock(new Date(capture.sunrise)).slice(0, 5), url: capture.url,
|
||
};
|
||
const text = (capture.test ? config.testPrefix : '') + config.template.join('\n').replace(/\{(\w+)\}/g, (_, key: string) => {
|
||
if (!(key in values)) throw new Error(`Unknown caption placeholder: ${key}`);
|
||
return values[key];
|
||
});
|
||
if (!text.trim() || [...text].length > THREADS.captionMaxLength) throw new Error('Caption must contain 1–500 characters');
|
||
return { text, condition };
|
||
}
|