Skip to content

Commit 9c56dba

Browse files
committed
Implement CCSDS CSV OMM (I love excel)
1 parent 6b236ca commit 9c56dba

5 files changed

Lines changed: 84 additions & 5 deletions

File tree

src/main.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { wrap } from "@bogeychan/elysia-logger";
44

55
import tleRoute from "./routes/tle";
66
import jsonRoute from "./routes/json";
7+
import csvRoute from "./routes/csv";
78
import noradRoute from "./routes/norad";
89

910
import index from "./pub/index.tsx";
@@ -24,16 +25,19 @@ new Elysia()
2425
for (const group of config.allowedGroups) {
2526
const tleTimestamp = await kv.get(`${group}_timestamp_tle`);
2627
const jsonTimestamp = await kv.get(`${group}_timestamp_json`);
28+
const csvTimestamp = await kv.get(`${group}_timestamp_csv`);
2729
const lastUpdateTle = tleTimestamp ? new Date(tleTimestamp).toISOString() : "Never";
2830
const lastUpdateJson = jsonTimestamp ? new Date(jsonTimestamp).toISOString() : "Never";
29-
activeGroups.push({ name: group, lastUpdateTle, lastUpdateJson });
31+
const lastUpdateCsv = csvTimestamp ? new Date(csvTimestamp).toISOString() : "Never";
32+
activeGroups.push({ name: group, lastUpdateTle, lastUpdateJson, lastUpdateCsv });
3033
}
3134
return index({ activeGroups, cacheDuration: config.cacheDuration, maxReq: config.rateLimitMaxRequests, maxReqWindow: config.rateLimitWindow, version });
3235
})
3336

3437
// Subroutes registers
3538
.use(tleRoute) // Import TLE routes
3639
.use(jsonRoute) // Import JSON routes
40+
.use(csvRoute) // Import CSV routes
3741
.use(noradRoute) // Import NORAD routes
3842

3943
.listen(config.port, () => {

src/pub/index.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const index = ({ activeGroups, cacheDuration, maxReq, maxReqWindow, version }: { activeGroups: { name: string; lastUpdateTle: string; lastUpdateJson: string }[]; cacheDuration: number; maxReq: number; maxReqWindow: number; version: string }) => (
1+
const index = ({ activeGroups, cacheDuration, maxReq, maxReqWindow, version }: { activeGroups: { name: string; lastUpdateTle: string; lastUpdateJson: string; lastUpdateCsv: string }[]; cacheDuration: number; maxReq: number; maxReqWindow: number; version: string }) => (
22
<html lang="en">
33
<head>
44
<meta charset="UTF-8" />
@@ -26,7 +26,7 @@ const index = ({ activeGroups, cacheDuration, maxReq, maxReqWindow, version }: {
2626
<p>This is a simple web app to cache and return TLEs from Celestrak, so you can avoid getting blocked by their servers when you need to get TLEs for a large number of satellites (Or you testing shit).</p>
2727

2828
<p>
29-
Currently supported formats are <strong>3LE</strong> under <code>/tle/[group]</code> and <strong>JSON CCSDS OMM</strong> under <code>/json/[group]</code>
29+
Currently supported formats are <strong>3LE</strong> under <code>/tle/[group]</code>, <strong>JSON CCSDS OMM</strong> under <code>/json/[group]</code>, and <strong>CSV</strong> under <code>/csv/[group]</code>
3030
</p>
3131

3232
<p>
@@ -41,12 +41,14 @@ const index = ({ activeGroups, cacheDuration, maxReq, maxReqWindow, version }: {
4141
<th>TLE (group)</th>
4242
<th>Last Update (TLE)</th>
4343
<th>Last Update (JSON)</th>
44+
<th>Last Update (CSV)</th>
4445
</tr>
4546
{activeGroups.map((group) => (
4647
<tr>
4748
<td>{group.name}</td>
4849
<td>{group.lastUpdateTle}</td>
4950
<td>{group.lastUpdateJson}</td>
51+
<td>{group.lastUpdateCsv}</td>
5052
</tr>
5153
))}
5254
</table>

src/pub/styles.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ body {
55

66
table {
77
border-collapse: collapse;
8-
width: 60%;
8+
width: 80%;
99
}
1010

1111
th,

src/routes/csv.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { Elysia } from "elysia";
2+
3+
import limiter from "../utils/ratelimiter";
4+
import config from "../utils/config";
5+
import kv from "../utils/kv";
6+
import log from "../utils/logger";
7+
import tleFetcher from "../utils/tleFetcher";
8+
9+
const tleRoute = new Elysia({ prefix: "/csv" })
10+
.use(limiter)
11+
.get("/", () => {
12+
return new Response(`Allowed groups: ${config.allowedGroups.join(", ")}`, { status: 200, headers: { "Content-Type": "text/plain" } });
13+
})
14+
.get("/:group", async (ctx) => {
15+
const group = ctx.params.group;
16+
if (config.allowedGroups.includes(group) === false) {
17+
return new Response(`Group "${group}" is not allowed.`, { status: 403 });
18+
}
19+
20+
let timestamp = await kv.get(`${group}_timestamp_csv`);
21+
const now = Date.now();
22+
const staleDuration = group === "active" ? config.cacheActiveDuration : config.cacheDuration;
23+
const isStale = timestamp ? now - timestamp > staleDuration : true;
24+
const lastFetchedHeader = new Date(ctx.request.headers.get("If-Modified-Since") || 0).getTime();
25+
26+
if (lastFetchedHeader && timestamp && lastFetchedHeader <= timestamp && !isStale) {
27+
log.debug(`TLEs for group "${group}", format "csv" not modified since last fetch. Returning 304.`);
28+
return new Response(null, { status: 304, headers: { "Last-Modified": new Date(timestamp).toUTCString(), "Cache-Control": `max-age=${group === "active" ? Math.ceil((config.cacheActiveDuration - (now - timestamp)) / 1000) : Math.ceil((config.cacheDuration - (now - timestamp)) / 1000)}` } });
29+
}
30+
31+
let tle = await kv.get(`${group}_csv`);
32+
33+
if (!tle) {
34+
log.debug(`No cached TLEs for group "${group}" in format "csv". Fetching from Celestrak...`);
35+
tle = await tleFetcher(group, "csv");
36+
timestamp = now;
37+
kv.set(`${group}_csv`, tle);
38+
kv.set(`${group}_timestamp_csv`, timestamp);
39+
} else if (isStale) {
40+
log.debug(`TLEs for group "${group}", format "csv" are stale. Fetching fresh TLEs...`);
41+
tle = await tleFetcher(group, "csv");
42+
timestamp = now;
43+
kv.set(`${group}_csv`, tle);
44+
kv.set(`${group}_timestamp_csv`, timestamp);
45+
} else {
46+
log.debug(`Serving cached TLEs for group "${group}" in format "csv".`);
47+
}
48+
49+
return new Response(tle, {
50+
headers: { "Content-Type": "text/plain", "Last-Modified": new Date(timestamp).toUTCString(), "Cache-Control": `max-age=${group === "active" ? Math.ceil((config.cacheActiveDuration - (now - timestamp)) / 1000) : Math.ceil((config.cacheDuration - (now - timestamp)) / 1000)}` },
51+
});
52+
})
53+
.get("/:group/status", async (ctx) => {
54+
const group = ctx.params.group;
55+
if (config.allowedGroups.includes(group) === false) {
56+
return new Response(`Group "${group}" is not allowed.`, { status: 403 });
57+
}
58+
59+
const timestamp = await kv.get(`${group}_timestamp_csv`);
60+
if (!timestamp) {
61+
return new Response(`No cached TLEs for group "${group}".`, { status: 404 });
62+
}
63+
64+
const now = Date.now();
65+
const age = Math.floor((now - timestamp) / 1000);
66+
const cacheDuration = group === "active" ? config.cacheActiveDuration : config.cacheDuration;
67+
const isStale = age > cacheDuration / 1000;
68+
const nextUpdate = new Date(timestamp + cacheDuration).toUTCString();
69+
70+
return new Response(`Group: ${group}\nLast Updated: ${new Date(timestamp).toUTCString()}\nAge: ${age} seconds\nStatus: ${isStale ? "Stale" : "Fresh"}\nNext Update: ${nextUpdate}`, { status: 200, headers: { "Content-Type": "text/plain" } });
71+
});
72+
73+
export default tleRoute;

src/utils/tleFetcher.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import kv from "./kv";
33

44
import { version } from "../../package.json";
55

6-
async function fetchTle(group: string, format: "tle" | "json" = "tle"): Promise<string> {
6+
async function fetchTle(group: string, format: "tle" | "json" | "csv" = "tle"): Promise<string> {
77
const url = `https://celestrak.org/NORAD/elements/gp.php?GROUP=${group}&FORMAT=${format}`;
88
log.debug(`Fetching TLEs for group "${group}", format "${format}" from Celestrak...`);
99
try {

0 commit comments

Comments
 (0)