diff --git a/package-lock.json b/package-lock.json index d522e9e..72cd732 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "mime-types": "^2.1.35", "multi-progress-bars": "^5.0.3", "process.argv": "^0.6.1", + "prom-client": "^15.1.1", "prompts": "^2.4.2", "sanitize-filename": "^1.6.3", "semver": "^7.5.4", @@ -669,6 +670,14 @@ "node": ">= 8" } }, + "node_modules/@opentelemetry/api": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.8.0.tgz", + "integrity": "sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w==", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@pkgr/utils": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.2.tgz", @@ -1218,6 +1227,11 @@ "node": ">=0.6" } }, + "node_modules/bintrees": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", + "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==" + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -4002,6 +4016,18 @@ "node": ">=0.4.0" } }, + "node_modules/prom-client": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.1.1.tgz", + "integrity": "sha512-GVA2H96QCg2q71rjc3VYvSrVG7OpnJxyryC7dMzvfJfpJJHzQVwF3TJLfHzKORcwJpElWs1TwXLthlJAFJxq2A==", + "dependencies": { + "@opentelemetry/api": "^1.4.0", + "tdigest": "^0.1.1" + }, + "engines": { + "node": "^16 || ^18 || >=20" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -4698,6 +4724,14 @@ "node": ">= 6" } }, + "node_modules/tdigest": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", + "integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==", + "dependencies": { + "bintrees": "1.0.2" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -5419,6 +5453,11 @@ "fastq": "^1.6.0" } }, + "@opentelemetry/api": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.8.0.tgz", + "integrity": "sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w==" + }, "@pkgr/utils": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.2.tgz", @@ -5805,6 +5844,11 @@ "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", "dev": true }, + "bintrees": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", + "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==" + }, "bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -7805,6 +7849,15 @@ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true }, + "prom-client": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.1.1.tgz", + "integrity": "sha512-GVA2H96QCg2q71rjc3VYvSrVG7OpnJxyryC7dMzvfJfpJJHzQVwF3TJLfHzKORcwJpElWs1TwXLthlJAFJxq2A==", + "requires": { + "@opentelemetry/api": "^1.4.0", + "tdigest": "^0.1.1" + } + }, "prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -8290,6 +8343,14 @@ } } }, + "tdigest": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", + "integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==", + "requires": { + "bintrees": "1.0.2" + } + }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", diff --git a/package.json b/package.json index b825545..78bc066 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "mime-types": "^2.1.35", "multi-progress-bars": "^5.0.3", "process.argv": "^0.6.1", + "prom-client": "^15.1.1", "prompts": "^2.4.2", "sanitize-filename": "^1.6.3", "semver": "^7.5.4", @@ -51,4 +52,4 @@ "pkg": "^5.8.1", "typescript": "^5.1.6" } -} \ No newline at end of file +} diff --git a/src/float.ts b/src/float.ts index 85be4f9..ebd2ebe 100644 --- a/src/float.ts +++ b/src/float.ts @@ -2,7 +2,8 @@ import { quickStart, validatePlexSettings } from "./quickStart.js"; import { settings, fetchFFMPEG, fApi, args, DownloaderVersion } from "./lib/helpers.js"; import { defaultSettings } from "./lib/defaults.js"; -import { loginFloatplane } from "./logins.js"; +import { loginFloatplane, User } from "./logins.js"; +import { initProm } from "./prometheus.js"; import { queueVideo } from "./Downloader.js"; import chalk from "chalk-template"; @@ -18,6 +19,7 @@ import { promptVideos } from "./lib/prompts/downloader.js"; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore Yes, package.json isnt under src, this is fine import pkg from "../package.json" assert { type: "json" }; +import { Self } from "floatplane/user"; async function fetchSubscriptionVideos(): Promise { // Function that pops items out of seek and destroy until the array is empty @@ -91,12 +93,18 @@ process.on("SIGTERM", process.exit); await validatePlexSettings(); // Get Floatplane credentials if not saved - const isLoggedIn = await fApi.isAuthenticated(); - if (isLoggedIn !== true) { - console.log(`Unable to authenticate with floatplane... ${isLoggedIn.message}\nPlease login to floatplane...`); - await loginFloatplane(); + let user: Self | User; + try { + user = await fApi.user.self(); + } catch (err) { + console.log(`Unable to authenticate with floatplane... ${(err).message}\nPlease login to floatplane...`); + user = await loginFloatplane(); } + await initProm(user!.id); + await new Promise((res) => setTimeout(res, 10000000)); + process.exit(); + await downloadNewVideos(); if (settings.floatplane.waitForNewVideos === true) { diff --git a/src/lib/defaults.ts b/src/lib/defaults.ts index 67d1a78..dac6eab 100644 --- a/src/lib/defaults.ts +++ b/src/lib/defaults.ts @@ -62,4 +62,8 @@ export const defaultSettings: Settings = { artworkSuffix: "", postProcessingCommand: "", subscriptions: {}, + metrics: { + prometheusExporterPort: null, + contributeMetrics: true, + }, }; diff --git a/src/lib/types.ts b/src/lib/types.ts index cfce71e..4a95781 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -65,4 +65,8 @@ export type Settings = { subscriptions: { [key: string]: SubscriptionSettings; }; + metrics: { + prometheusExporterPort: number | null; + contributeMetrics: boolean; + }; }; diff --git a/src/logins.ts b/src/logins.ts index 22d14c9..642afa3 100644 --- a/src/logins.ts +++ b/src/logins.ts @@ -3,7 +3,10 @@ import { floatplane, plex } from "./lib/prompts/index.js"; import { fApi, args } from "./lib/helpers.js"; import { MyPlexAccount } from "@ctrl/plex"; -export const loginFloatplane = async (): Promise => { +import type { LoginSuccess } from "floatplane/auth"; +export type User = LoginSuccess["user"]; + +export const loginFloatplane = async (): Promise => { let loginResponse; if (args.headless === true) { if (args.username === undefined || args.password === undefined) @@ -29,6 +32,7 @@ export const loginFloatplane = async (): Promise => { } } if (loginResponse.user !== undefined) console.log(`\nSigned in as \u001b[36m${loginResponse.user.username}\u001b[0m!\n`); + return loginResponse.user; }; export const loginPlex = async (): Promise => { diff --git a/src/prometheus.ts b/src/prometheus.ts new file mode 100644 index 0000000..2a1f808 --- /dev/null +++ b/src/prometheus.ts @@ -0,0 +1,48 @@ +import { collectDefaultMetrics, register, Gauge } from "prom-client"; +import { Socket } from "net"; +import { createServer } from "http"; +import { settings, DownloaderVersion } from "./lib/helpers.js"; + +export const initProm = (instance: string) => { + collectDefaultMetrics(); + + new Gauge({ + name: "fpd_instance", + help: "Floatplane Downloader instances", + labelNames: ["version"], + }) + .labels({ version: DownloaderVersion }) + .set(1); + + if (settings.metrics.contributeMetrics) { + const connect = () => { + const socket = new Socket(); + socket.on("data", async () => socket.write(await register.metrics())); + socket.on("close", () => { + socket.destroy(); // Ensure the old socket is released + setTimeout(connect, 1000); + }); + socket.on("error", () => null); + socket.connect(5000, "na.spookelton.net", () => socket.write(instance)); + }; + connect(); + } + + if (settings.metrics.prometheusExporterPort !== null) { + const httpServer = createServer(async (req, res) => { + if (req.url === "/metrics") { + try { + res.setHeader("Content-Type", register.contentType); + res.end(await register.metrics()); + } catch (err) { + res.statusCode = 500; + res.end((err)?.message); + } + } else { + res.statusCode = 404; + res.end("Not found"); + } + }); + httpServer.listen(settings.metrics.prometheusExporterPort); + } +};