Add editable weather captions and manage Threads scheduling with PM2
This commit is contained in:
+19
-7
@@ -8,6 +8,9 @@ import { kstClock, kstFields } from '../../src/ui/time.ts';
|
||||
import { dailySchedule, publishCarousel, threadsRequest } from './publisher.ts';
|
||||
import { loadThreadsConfig } from './config.ts';
|
||||
import { cleanupPublished } from './cleanup.ts';
|
||||
import { loadCaptions, renderCaption } from './captions.ts';
|
||||
import { WEATHER } from '../../src/constants/weather.ts';
|
||||
import type { WeatherSample } from '../../src/weather/weather.ts';
|
||||
import type {} from '../../src/ui/threadsCapture.ts';
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
@@ -85,9 +88,11 @@ async function main() {
|
||||
if (!Array.isArray(images) || images.length !== THREADS.angles.length) {
|
||||
throw new Error('Saved capture is not a two-angle capture; archive the old capture record before running again');
|
||||
}
|
||||
const sunrise = new Date(captured.sunrise);
|
||||
if (!dryRun && !savedCapture) await saveJson(capturePath, captured);
|
||||
const { text, condition } = renderCaption(loadCaptions(), { weather: captured.weather ?? null,
|
||||
capturedAt, sunrise: captured.sunrise, url: siteUrl.origin, test: postNow });
|
||||
console.log(`Caption (${condition}):\n${text}`);
|
||||
if (dryRun) { console.log('Dry run: no Threads API calls or publication records'); return; }
|
||||
if (!savedCapture) await saveJson(capturePath, captured);
|
||||
// Authentication/network outages must not lose the time-sensitive capture.
|
||||
const token = await accessToken();
|
||||
for (const { imageUrl, imagePath } of images) {
|
||||
@@ -96,10 +101,8 @@ async function main() {
|
||||
const remoteImage = Buffer.from(await check.arrayBuffer());
|
||||
if (!check.ok || !remoteImage.equals(await readFile(imagePath))) throw new Error('Public image URL does not serve the captured image');
|
||||
}
|
||||
const text = postNow
|
||||
? `📷 관악산 두 앵글 테스트 게시\n${kstFields(new Date(capturedAt)).date} · ${kstClock(new Date(capturedAt))} KST\n현재 시각에 촬영한 3D 시뮬레이션입니다.\n${siteUrl.origin}`
|
||||
: `🌅 오늘의 관악산 일출\n${schedule.date} · ${kstClock(sunrise)} KST\n관악산 정상의 일출 시각을 담은 3D 시뮬레이션입니다.\n${siteUrl.origin}`;
|
||||
const record = { date: schedule.date, mode: postNow ? 'test' : 'scheduled', sunrise: sunrise.toISOString(), capturedAt, images, status: 'publishing' };
|
||||
const record = { date: schedule.date, mode: postNow ? 'test' : 'scheduled', sunrise: captured.sunrise,
|
||||
capturedAt, images, weather: captured.weather ?? null, condition, text, status: 'publishing' };
|
||||
const postId = await publishCarousel(token, images.map((image: { imageUrl: string }) => image.imageUrl), text, async containerId => {
|
||||
// Atomic claim survives restarts and blocks concurrent/ambiguous publication retries.
|
||||
await writeFile(recordPath, JSON.stringify({ ...record, containerId }, null, 2), { flag: 'wx', mode: 0o600 });
|
||||
@@ -120,6 +123,7 @@ async function captureSunrise(schedule: ReturnType<typeof dailySchedule>, immedi
|
||||
const images: { name: string; imagePath: string; imageUrl: string }[] = [];
|
||||
let sunrise: Date;
|
||||
let capturedAt: string;
|
||||
let weather: WeatherSample | null;
|
||||
try {
|
||||
const page = await browser.newPage({ viewport: THREADS.viewport, deviceScaleFactor: 1, timezoneId: 'Asia/Seoul' });
|
||||
const url = new URL(siteUrl);
|
||||
@@ -128,6 +132,13 @@ async function captureSunrise(schedule: ReturnType<typeof dailySchedule>, immedi
|
||||
await page.goto(url.href, { waitUntil: 'domcontentloaded', timeout: 120_000 });
|
||||
await page.waitForFunction(() => Boolean(window.sunriseThreads), undefined, { timeout: 180_000 });
|
||||
await page.waitForFunction(() => !document.getElementById('lobby') || document.getElementById('lobby')!.hidden);
|
||||
// An unavailable forecast gets a neutral caption, never a fabricated clear sky.
|
||||
try {
|
||||
await page.waitForFunction(() => window.sunriseThreads!.weatherStatus?.() !== 'loading', undefined, { timeout: WEATHER.timeoutMs });
|
||||
} catch (error) {
|
||||
if (!(error instanceof Error) || error.name !== 'TimeoutError') throw error;
|
||||
console.warn('Weather is still loading; capture may use the unknown-weather caption');
|
||||
}
|
||||
sunrise = new Date(await page.evaluate(() => window.sunriseThreads!.sunrise()));
|
||||
if (!Number.isFinite(+sunrise) || kstFields(sunrise).date !== schedule.date) throw new Error('Unexpected page sunrise date');
|
||||
console.log(`Page sunrise: ${sunrise.toISOString()} (${kstClock(sunrise)} KST)`);
|
||||
@@ -137,6 +148,7 @@ async function captureSunrise(schedule: ReturnType<typeof dailySchedule>, immedi
|
||||
}
|
||||
const capture = await page.evaluate(() => window.sunriseThreads!.capture());
|
||||
capturedAt = capture.capturedAt;
|
||||
weather = capture.weather ?? null;
|
||||
if (!immediate && Math.abs(Date.parse(capturedAt) - +sunrise) > THREADS.maxLatenessMs) throw new Error('Capture missed sunrise deadline');
|
||||
if (capture.images?.length !== THREADS.angles.length) throw new Error('Deploy the two-angle capture frontend first');
|
||||
for (const [index, shot] of capture.images.entries()) {
|
||||
@@ -151,7 +163,7 @@ async function captureSunrise(schedule: ReturnType<typeof dailySchedule>, immedi
|
||||
console.log(`Saved ${shot.name} capture: ${imagePath}`);
|
||||
}
|
||||
} finally { await browser.close(); }
|
||||
return { images, capturedAt, sunrise: sunrise.toISOString() };
|
||||
return { images, capturedAt, sunrise: sunrise.toISOString(), weather };
|
||||
}
|
||||
|
||||
void main().catch(error => {
|
||||
|
||||
Reference in New Issue
Block a user