Repository navigation
Expand file tree
/
Copy pathserver.ts
More file actions
155 lines (149 loc) · 6.18 KB
/
Copy pathserver.ts
File metadata and controls
155 lines (149 loc) · 6.18 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
import { ensureConfigDir, booksDbPath, port } from "./src/config";
import { openDatabase } from "./src/db";
import { createPodibleFetchHandler } from "./src/http";
import { pingPlexOwnerToken } from "./src/plex";
import { BooksRepo } from "./src/repo";
import { BackgroundWorker } from "./src/runtime/background-worker";
import { AcquisitionEvents, acquisitionEventsPath, acquisitionTransferPath } from "./src/runtime/acquisition-events";
import { readAutobrrConfig, reconcileAutobrrWebhook, type AutobrrConfig } from "./src/media/autobrr";
import type { HTMLBundle } from "bun";
export async function startServer(frontend?: HTMLBundle) {
const startTime = Date.now();
await ensureConfigDir();
const db = openDatabase(booksDbPath);
let server: ReturnType<typeof Bun.serve> | undefined;
let stopping = false;
let plexPingTimer: ReturnType<typeof setInterval> | undefined;
let autobrrTimer: ReturnType<typeof setTimeout> | undefined;
const autobrrLifetime = new AbortController();
const acquisitionEvents = new AcquisitionEvents();
const background = new BackgroundWorker({ databasePath: booksDbPath, onAcquisitionChange: () => acquisitionEvents.notify(), onFailure: error => {
console.error("[background] execution worker failed", error.message);
void shutdown(1);
} });
async function shutdown(exitCode = 0): Promise<void> {
if (stopping) return;
stopping = true;
clearInterval(plexPingTimer);
clearTimeout(autobrrTimer);
autobrrLifetime.abort();
acquisitionEvents.close();
const deadline = setTimeout(() => process.exit(exitCode || 1), 8_000);
deadline.unref();
try {
const [, result] = await Promise.all([server?.stop(), background.stop()]);
if (result.forced) console.warn("[background] worker terminated after shutdown deadline; durable leases will recover work");
db.close();
clearTimeout(deadline);
process.exit(exitCode);
} catch {
console.error("[background] shutdown incomplete; exiting without starting another engine");
process.exit(1);
}
}
const onSignal = () => void shutdown();
process.on("SIGINT", onSignal);
process.on("SIGTERM", onSignal);
try {
await background.start();
if (stopping) throw new Error("Server stopped during background startup");
// Construct after the worker's durable journal migration, never cache legacy schema on main.
const repo = new BooksRepo(db);
repo.ensureSettings();
const current = repo.getSettings();
// Keep the Plex owner token alive; this existing asynchronous request does not run jobs.
async function pingPlexAndLog(): Promise<void> {
const settings = repo.getSettings();
if (!settings.auth.plex.ownerToken) return;
const ok = await pingPlexOwnerToken(settings);
console.log(`[plex] owner token ping ${ok ? "ok" : "FAILED - admin must re-link Plex"}`);
}
void pingPlexAndLog();
const PLEX_PING_INTERVAL_MS = 24 * 60 * 60 * 1000;
plexPingTimer = setInterval(
() => void pingPlexAndLog(),
PLEX_PING_INTERVAL_MS,
);
let autobrr: AutobrrConfig | undefined;
try {
autobrr = readAutobrrConfig(process.env) ?? undefined;
} catch (error) {
console.warn("[autobrr] " + (error as Error).message + "; RSS remains enabled");
}
const fetchHandler = createPodibleFetchHandler(repo, startTime, undefined, { autobrr, acquisitionEvents });
server = Bun.serve({
port,
idleTimeout: 60,
...(frontend
? {
routes: {
"/": frontend,
"/library": frontend,
"/add": frontend,
"/book/:bookId": frontend,
"/series": frontend,
"/author": frontend,
"/movies": frontend,
"/movies/:id": frontend,
"/requests": frontend,
"/requests/:id": frontend,
"/tv/:id": frontend,
"/tv/requests/:id": frontend,
"/tv/library/:id": frontend,
"/recovery": frontend,
"/stewardship": frontend,
"/tv-retention": frontend,
"/movie-dual-write": frontend,
},
development: { hmr: true, console: true },
}
: { development: false }),
fetch: (request, httpServer) => {
if (stopping || background.state !== "ready") return new Response("Background execution unavailable", { status: 503 });
if (request.method === "GET" && [acquisitionEventsPath, acquisitionTransferPath].includes(new URL(request.url).pathname)) httpServer.timeout(request, 0);
return fetchHandler(request);
},
});
if (autobrr) {
const config = autobrr;
let previous: string | undefined;
async function register() {
let status: string;
let delay: number;
try {
await reconcileAutobrrWebhook(config, fetch, autobrrLifetime.signal);
status = "Release webhook registered";
delay = 5 * 60_000;
} catch (error) {
const detail = error instanceof Error && /^(Autobrr notification API returned HTTP \d{3}|Conflicting Podible notification in autobrr)$/.test(error.message)
? error.message : "Could not reconcile the release webhook";
status = detail + "; retrying, RSS remains enabled";
delay = 60_000;
}
if (autobrrLifetime.signal.aborted) return;
if (status !== previous) console.log("[autobrr] " + status);
previous = status;
autobrrTimer = setTimeout(() => void register(), delay);
autobrrTimer.unref();
}
void register();
}
const localBase = `http://localhost${server.port === 80 ? "" : `:${server.port}`}`;
console.log(`Podible backend listening on ${localBase}`);
console.log(`Library root: ${current.libraryRoot}`);
console.log(`RPC endpoint: ${localBase}/rpc`);
console.log(`Home: ${localBase}/`);
return server;
} catch (error) {
clearInterval(plexPingTimer);
clearTimeout(autobrrTimer);
autobrrLifetime.abort();
process.off("SIGINT", onSignal);
process.off("SIGTERM", onSignal);
await server?.stop();
await background.stop();
db.close();
throw error;
}
}
if (import.meta.main) await startServer();