server/src/ll2.ts
80 lines · 2992 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/** Recent launches, for the international designator, which is assigned only after liftoff. */
6export const PREVIOUS_URL = "https://ll.thespacedevs.com/2.3.0/launches/previous/?limit=10&mode=normal";
7
8export interface Launch {
9 id: string;
10 name: string;
11 net: string;
12 windowStart: string | null;
13 windowEnd: string | null;
14 status: string;
15 statusName: string;
16 provider: string;
17 rocket: string;
18 mission: string | null;
19 missionType: string | null;
20 missionDescription: string | null;
21 orbit: string | null;
22 pad: string;
23 location: string;
24 padLatitude: number | null;
25 padLongitude: number | null;
26 lastUpdated: string;
27 /** International designator, e.g. "2026-232"; null before launch. */
28 designator: string | null;
29}
30
31interface Raw {
32 id: string;
33 name: string;
34 net: string;
35 window_start?: string | null;
36 window_end?: string | null;
37 last_updated: string;
38 launch_designator?: string | null;
39 status: { abbrev: string; name: string };
40 launch_service_provider: { name: string };
41 rocket: { configuration: { full_name?: string; name: string } };
42 mission?: { name?: string; type?: string; description?: string; orbit?: { abbrev?: string } | null } | null;
43 pad: { name: string; latitude?: string | number | null; longitude?: string | number | null; location: { name: string } };
44}
45
46const num = (v: string | number | null | undefined) => (v == null || v === "" ? null : Number(v));
47
48export function normalize(r: Raw): Launch {
49 return {
50 id: r.id,
51 name: r.name,
52 net: r.net,
53 windowStart: r.window_start ?? null,
54 windowEnd: r.window_end ?? null,
55 status: r.status.abbrev,
56 statusName: r.status.name,
57 provider: r.launch_service_provider.name,
58 rocket: r.rocket.configuration.full_name ?? r.rocket.configuration.name,
59 mission: r.mission?.name ?? null,
60 missionType: r.mission?.type ?? null,
61 missionDescription: r.mission?.description?.trim() ?? null,
62 orbit: r.mission?.orbit?.abbrev ?? null,
63 pad: r.pad.name,
64 location: r.pad.location.name,
65 padLatitude: num(r.pad.latitude),
66 padLongitude: num(r.pad.longitude),
67 lastUpdated: r.last_updated,
68 designator: r.launch_designator ?? null,
69 };
70}
71
72/** `apiKey` lifts the anonymous per-IP limit, which Cloudflare's shared egress IPs often exhaust (HTTP 429). */
73export async function fetchLaunches(url: string, apiKey?: string): Promise<Launch[]> {
74 const headers: Record<string, string> = { "user-agent": "moonrise-server (sh.krz.moonrise)" };
75 if (apiKey) headers.authorization = `Token ${apiKey}`;
76 const res = await fetch(url, { headers });
77 if (!res.ok) throw new Error(`LL2 ${res.status}`);
78 const body = (await res.json()) as { results: Raw[] };
79 return body.results.map(normalize);
80}