server/src/ll2.ts
74 lines · 2604 bytes
1// Launch Library 2 (The Space Devs). Anonymous limit: 15 requests per hour.
2// Images are not used: their licences are often "Unknown".
3
4export const UPCOMING_URL = "https://ll.thespacedevs.com/2.3.0/launches/upcoming/?limit=20&mode=normal";
5
6export interface Launch {
7 id: string;
8 name: string;
9 net: string;
10 windowStart: string | null;
11 windowEnd: string | null;
12 status: string;
13 statusName: string;
14 provider: string;
15 rocket: string;
16 mission: string | null;
17 missionType: string | null;
18 missionDescription: string | null;
19 orbit: string | null;
20 pad: string;
21 location: string;
22 padLatitude: number | null;
23 padLongitude: number | null;
24 lastUpdated: string;
25}
26
27interface Raw {
28 id: string;
29 name: string;
30 net: string;
31 window_start?: string | null;
32 window_end?: string | null;
33 last_updated: string;
34 status: { abbrev: string; name: string };
35 launch_service_provider: { name: string };
36 rocket: { configuration: { full_name?: string; name: string } };
37 mission?: { name?: string; type?: string; description?: string; orbit?: { abbrev?: string } | null } | null;
38 pad: { name: string; latitude?: string | number | null; longitude?: string | number | null; location: { name: string } };
39}
40
41const num = (v: string | number | null | undefined) => (v == null || v === "" ? null : Number(v));
42
43export function normalize(r: Raw): Launch {
44 return {
45 id: r.id,
46 name: r.name,
47 net: r.net,
48 windowStart: r.window_start ?? null,
49 windowEnd: r.window_end ?? null,
50 status: r.status.abbrev,
51 statusName: r.status.name,
52 provider: r.launch_service_provider.name,
53 rocket: r.rocket.configuration.full_name ?? r.rocket.configuration.name,
54 mission: r.mission?.name ?? null,
55 missionType: r.mission?.type ?? null,
56 missionDescription: r.mission?.description?.trim() ?? null,
57 orbit: r.mission?.orbit?.abbrev ?? null,
58 pad: r.pad.name,
59 location: r.pad.location.name,
60 padLatitude: num(r.pad.latitude),
61 padLongitude: num(r.pad.longitude),
62 lastUpdated: r.last_updated,
63 };
64}
65
66/** `apiKey` lifts the anonymous per-IP limit, which Cloudflare's shared egress IPs often exhaust (HTTP 429). */
67export async function fetchUpcoming(apiKey?: string): Promise<Launch[]> {
68 const headers: Record<string, string> = { "user-agent": "moonrise-server (sh.krz.moonrise)" };
69 if (apiKey) headers.authorization = `Token ${apiKey}`;
70 const res = await fetch(UPCOMING_URL, { headers });
71 if (!res.ok) throw new Error(`LL2 ${res.status}`);
72 const body = (await res.json()) as { results: Raw[] };
73 return body.results.map(normalize);
74}