krz/moonrise

Night-sky companion for iPhone, Apple Watch and Vision Pro: what's up tonight, the Moon, planets, launches and missions, and a Library of the solar system. SwiftUI app with a Cloudflare Workers backend. astronomy cloudflare-workers ios night-sky space swiftui visionos watchos

server/src/ll2.ts

6c4b28384d65e09486e0ad62f2ac9d10401cc80c
moonrise/server/src/ll2.ts history · blame · raw

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}