|
|
|
@@ -9,12 +9,20 @@
|
|
|
|
|
// **Which entry is plex's.** ombi may list several Plex servers. The one this provision names is
|
|
|
|
|
// found by the server's own machineIdentifier, which plex answers at /identity — the same value
|
|
|
|
|
// ombi stored when an operator loaded the server in its settings screen. That entry's connection is
|
|
|
|
|
// brought in line; an entry for any other server is never touched. When no entry is this server's,
|
|
|
|
|
// one is added, named as plex names itself — it is the mesh's, so later runs keep it true.
|
|
|
|
|
// brought in line; an entry for any other server is never touched.
|
|
|
|
|
//
|
|
|
|
|
// **Only the connection, and only when it differs.** Host, port, TLS, base path and token. Whether
|
|
|
|
|
// Plex is enabled in ombi, watchlist import, the selected libraries, the batch size and everything
|
|
|
|
|
// else an operator chose are left exactly as they are.
|
|
|
|
|
// When no entry carries that identifier, an entry may still be this server reached another way:
|
|
|
|
|
// ace's ombi holds one loaded from an older server and later retyped to plex's public name, so its
|
|
|
|
|
// stored identifier is stale while its address answers as this plex. Each entry's OWN address is
|
|
|
|
|
// asked for /identity, and an entry plex itself answers for is this server's — adopted: its
|
|
|
|
|
// connection laid over and its identifier corrected (ombi builds its "view in Plex" links from it).
|
|
|
|
|
// Nothing is guessed: an entry whose address is unreachable, or answers as another server, is left
|
|
|
|
|
// as it was. Only when no entry is this server's either way is one added, named as plex names
|
|
|
|
|
// itself — it is the mesh's, so later runs keep it true.
|
|
|
|
|
//
|
|
|
|
|
// **Only the connection, and only when it differs.** Host, port, TLS, base path and token — and the
|
|
|
|
|
// identifier of an adopted entry. Whether Plex is enabled in ombi, watchlist import, the selected
|
|
|
|
|
// libraries, the batch size and everything else an operator chose are left exactly as they are.
|
|
|
|
|
//
|
|
|
|
|
// **A token plex refuses is never written.** Until the operator accepts the server's token for this
|
|
|
|
|
// pair, the mesh delivers a value it minted itself, which plex answers with 401 (or 400 on its own
|
|
|
|
@@ -116,8 +124,10 @@ export async function plexTakes(http: Http, want: PlexConnection): Promise<{ tak
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** The server's own machineIdentifier, which plex answers without a token. */
|
|
|
|
|
export async function plexIdentity(http: Http, want: PlexConnection): Promise<string> {
|
|
|
|
|
const res = await plexGet(http, want, "/identity", false);
|
|
|
|
|
export async function plexIdentity(http: Http, want: Pick<PlexConnection, "ip" | "port" | "ssl" | "subDir">): Promise<string> {
|
|
|
|
|
const host = want.ip.includes(":") && !want.ip.startsWith("[") ? `[${want.ip}]` : want.ip;
|
|
|
|
|
const base = `${want.ssl ? "https" : "http"}://${host}:${want.port}${want.subDir ? `/${want.subDir.replace(/^\/+|\/+$/g, "")}` : ""}`;
|
|
|
|
|
const res = await http.fetch(`${base}/identity`, { method: "GET", headers: { Accept: "application/json" } });
|
|
|
|
|
if (res.status !== 200) throw new Error(`plex answered ${res.status} at /identity`);
|
|
|
|
|
const body = JSON.parse(await res.text()) as { MediaContainer?: { machineIdentifier?: unknown } };
|
|
|
|
|
const id = body.MediaContainer?.machineIdentifier;
|
|
|
|
@@ -138,25 +148,57 @@ export function plexAcceptRemedy(from: string): string {
|
|
|
|
|
/** The batch size ombi's settings screen fills in for a server it adds ("150 by default"). */
|
|
|
|
|
const EPISODE_BATCH_SIZE = 150;
|
|
|
|
|
|
|
|
|
|
/** ombi's server entries, as its settings document holds them (null on a fresh ombi). */
|
|
|
|
|
export function serversOf(document: Record<string, unknown> | undefined): Record<string, unknown>[] {
|
|
|
|
|
const servers = document?.servers;
|
|
|
|
|
return Array.isArray(servers) ? (servers as Record<string, unknown>[]) : [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Which entries, holding another identifier, plex answers for at their own address — this server,
|
|
|
|
|
* reached another way. Asked only when no entry carries the identifier. An entry that cannot be
|
|
|
|
|
* asked is not this server's: nothing is guessed.
|
|
|
|
|
*/
|
|
|
|
|
export async function answeringAs(http: Http, servers: Record<string, unknown>[], machineIdentifier: string): Promise<Set<number>> {
|
|
|
|
|
const out = new Set<number>();
|
|
|
|
|
if (servers.some((s) => s?.machineIdentifier === machineIdentifier)) return out;
|
|
|
|
|
for (const [i, s] of servers.entries()) {
|
|
|
|
|
const ip = typeof s?.ip === "string" ? s.ip.trim() : "";
|
|
|
|
|
const port = Number(s?.port);
|
|
|
|
|
if (!ip || !Number.isInteger(port) || port <= 0 || port > 65535) continue;
|
|
|
|
|
const subDir = typeof s.subDir === "string" && s.subDir.trim() !== "" ? s.subDir : null;
|
|
|
|
|
try {
|
|
|
|
|
if ((await plexIdentity(http, { ip, port, ssl: Boolean(s.ssl), subDir })) === machineIdentifier) out.add(i);
|
|
|
|
|
} catch {
|
|
|
|
|
// unreachable, or not a plex: not this server's
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* ombi's Plex settings with this server's connection laid over them: every entry naming the
|
|
|
|
|
* server's machineIdentifier gets the connection, every other entry is left as it was, and when
|
|
|
|
|
* none names it one is added. Returns the document to save and the fields that changed.
|
|
|
|
|
* server's machineIdentifier — or adopted, its address answering as this server — gets the
|
|
|
|
|
* connection (an adopted one also the identifier), every other entry is left as it was, and when
|
|
|
|
|
* none is this server's one is added. Returns the document to save and the fields that changed.
|
|
|
|
|
*/
|
|
|
|
|
export function withPlexServer(
|
|
|
|
|
document: Record<string, unknown> | undefined,
|
|
|
|
|
machineIdentifier: string,
|
|
|
|
|
want: PlexConnection,
|
|
|
|
|
name: string,
|
|
|
|
|
adopted: ReadonlySet<number> = new Set(),
|
|
|
|
|
): { next: Record<string, unknown>; fields: string[]; added: boolean; entry: Record<string, unknown> } {
|
|
|
|
|
const doc = document ?? {};
|
|
|
|
|
const servers = Array.isArray(doc.servers) ? (doc.servers as Record<string, unknown>[]) : [];
|
|
|
|
|
const servers = serversOf(doc);
|
|
|
|
|
const fields = new Set<string>();
|
|
|
|
|
let entry: Record<string, unknown> | undefined;
|
|
|
|
|
const next = servers.map((s) => {
|
|
|
|
|
if (s?.machineIdentifier !== machineIdentifier) return s;
|
|
|
|
|
const next = servers.map((s, i) => {
|
|
|
|
|
const adopt = adopted.has(i) && s?.machineIdentifier !== machineIdentifier;
|
|
|
|
|
if (s?.machineIdentifier !== machineIdentifier && !adopt) return s;
|
|
|
|
|
for (const f of differingPlex(s, want)) fields.add(f);
|
|
|
|
|
const laid = { ...s, ip: want.ip, port: want.port, ssl: want.ssl, subDir: want.subDir, plexAuthToken: want.plexAuthToken };
|
|
|
|
|
if (adopt) fields.add("machineIdentifier");
|
|
|
|
|
const laid = { ...s, machineIdentifier, ip: want.ip, port: want.port, ssl: want.ssl, subDir: want.subDir, plexAuthToken: want.plexAuthToken };
|
|
|
|
|
entry ??= laid;
|
|
|
|
|
return laid;
|
|
|
|
|
});
|
|
|
|
@@ -201,7 +243,8 @@ export async function reconcilePlex(http: Http, ombi: Ombi, binding: Binding | u
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const document = (await ombiCall(http, ombi, "GET", "/Settings/Plex")) as Record<string, unknown> | undefined;
|
|
|
|
|
const laid = withPlexServer(document, machineIdentifier, want, name);
|
|
|
|
|
const adopted = await answeringAs(http, serversOf(document), machineIdentifier);
|
|
|
|
|
const laid = withPlexServer(document, machineIdentifier, want, name, adopted);
|
|
|
|
|
if (laid.fields.length > 0) {
|
|
|
|
|
const saved = await ombiCall(http, ombi, "POST", "/Settings/Plex", laid.next);
|
|
|
|
|
if (saved === false) return { app, result: "refused", problem: "ombi declined to save its Plex settings" };
|
|
|
|
|