diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml
index e0e5350..6f425f7 100644
--- a/.github/workflows/ci-build.yml
+++ b/.github/workflows/ci-build.yml
@@ -125,6 +125,9 @@ jobs:
- name: Install dependencies
run: npm ci
+ - name: Prepare Linux desktop patch bundle
+ run: npm run prepare:linux-desktop-bundle
+
- name: Build injector linux package
run: npm run dist:linux
diff --git a/.gitignore b/.gitignore
index da938ad..41b1f11 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,6 +16,17 @@ cache/
# Local app copy / extraction workspace
app_do_not_edit/
+do_not_edit/*
+!do_not_edit/fluxer/
+do_not_edit/fluxer/*
+!do_not_edit/fluxer/fluxer_desktop/
+!do_not_edit/fluxer/fluxer_desktop/**
+do_not_edit/fluxer/.git/
+do_not_edit/fluxer/fluxer_desktop/node_modules/
+do_not_edit/fluxer/fluxer_desktop/dist/*
+!do_not_edit/fluxer/fluxer_desktop/dist/main/
+do_not_edit/fluxer/fluxer_desktop/dist/main/*
+!do_not_edit/fluxer/fluxer_desktop/dist/main/index.js
MyPlugins/
MainFluxerApp/
# Logs
diff --git a/bridge-nw/app.js b/bridge-nw/app.js
index f2feebb..6366091 100644
--- a/bridge-nw/app.js
+++ b/bridge-nw/app.js
@@ -12,12 +12,61 @@ const startBtn = document.getElementById("startBtn");
const stopBtn = document.getElementById("stopBtn");
const healthBtn = document.getElementById("healthBtn");
const probeBtn = document.getElementById("probeBtn");
+const nerdModeToggleEl = document.getElementById("nerdModeToggle");
+const updateIntervalInputEl = document.getElementById("updateIntervalInput");
const statusEl = document.getElementById("status");
const logEl = document.getElementById("log");
let bridgeProc = null;
let tray = null;
let isQuitting = false;
+let healthMonitorTimer = null;
+const SETTINGS_FILE = path.join(getAppDataHome(), APP_NAME, "data", "bridge-settings.json");
+const DEFAULT_UPDATE_INTERVAL_MS = 10000;
+
+function normalizeUpdateIntervalMs(value) {
+ const n = Number.parseInt(String(value || ""), 10);
+ if (!Number.isFinite(n)) return DEFAULT_UPDATE_INTERVAL_MS;
+ return Math.max(1000, Math.min(60000, Math.round(n)));
+}
+
+function ensureBridgeDataDir() {
+ fs.mkdirSync(path.dirname(SETTINGS_FILE), { recursive: true });
+}
+
+function loadBridgeSettings() {
+ try {
+ if (!fs.existsSync(SETTINGS_FILE)) {
+ return { nerdModeEnabled: false, updateIntervalMs: DEFAULT_UPDATE_INTERVAL_MS };
+ }
+ const parsed = JSON.parse(fs.readFileSync(SETTINGS_FILE, "utf8"));
+ return {
+ nerdModeEnabled: Boolean(parsed && parsed.nerdModeEnabled),
+ updateIntervalMs: normalizeUpdateIntervalMs(parsed && parsed.updateIntervalMs)
+ };
+ } catch (_) {
+ return { nerdModeEnabled: false, updateIntervalMs: DEFAULT_UPDATE_INTERVAL_MS };
+ }
+}
+
+function saveBridgeSettings(nextSettings) {
+ ensureBridgeDataDir();
+ const payload = {
+ nerdModeEnabled: Boolean(nextSettings && nextSettings.nerdModeEnabled),
+ updateIntervalMs: normalizeUpdateIntervalMs(nextSettings && nextSettings.updateIntervalMs)
+ };
+ fs.writeFileSync(SETTINGS_FILE, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
+ return payload;
+}
+
+function applyBridgeSettingsToUi(settings) {
+ if (nerdModeToggleEl) {
+ nerdModeToggleEl.checked = Boolean(settings && settings.nerdModeEnabled);
+ }
+ if (updateIntervalInputEl) {
+ updateIntervalInputEl.value = String(normalizeUpdateIntervalMs(settings && settings.updateIntervalMs));
+ }
+}
function getBundleRoot() {
try {
@@ -44,6 +93,12 @@ function getTrayIconPath() {
function resolveNodeExec() {
const npmNode = String(process.env.npm_node_execpath || "").trim();
if (npmNode && fs.existsSync(npmNode)) return npmNode;
+ const runtimeNodeCandidates = [
+ path.join(getBridgeRuntimeRoot(), "node"),
+ path.join(getBridgeRuntimeRoot(), "node.exe")
+ ];
+ const runtimeNode = runtimeNodeCandidates.find((p) => fs.existsSync(p));
+ if (runtimeNode) return runtimeNode;
if (process.platform === "win32") {
const localNodeCandidates = [
path.join(path.resolve(process.cwd(), ".."), "node.exe"),
@@ -112,6 +167,26 @@ function getBundledBridgeExePath() {
return candidates.find((p) => fs.existsSync(p)) || "";
}
+function getBundledNodePath() {
+ const bundleRoot = getBundleRoot();
+ const candidates = process.platform === "win32"
+ ? [
+ path.join(bundleRoot, "node.exe"),
+ path.join(bundleRoot, "bridge-nw", "node.exe"),
+ path.join(bundleRoot, "package.nw", "node.exe"),
+ path.join(path.dirname(process.execPath), "node.exe"),
+ path.join(path.dirname(process.execPath), "package.nw", "node.exe")
+ ]
+ : [
+ path.join(bundleRoot, "node"),
+ path.join(bundleRoot, "bridge-nw", "node"),
+ path.join(bundleRoot, "package.nw", "node"),
+ path.join(path.dirname(process.execPath), "node"),
+ path.join(path.dirname(process.execPath), "package.nw", "node")
+ ];
+ return candidates.find((p) => fs.existsSync(p)) || "";
+}
+
function materializeBridgeRuntimeFiles() {
const sourceBridgeScript = getBundledBridgeScriptPath();
if (!sourceBridgeScript) return "";
@@ -138,6 +213,16 @@ function materializeBridgeRuntimeFiles() {
fs.copyFileSync(bundledEnv, path.join(runtimeBridgeEnvDir, ".env"));
}
+ const bundledNode = getBundledNodePath();
+ if (bundledNode) {
+ const runtimeNodeName = process.platform === "win32" ? "node.exe" : "node";
+ const runtimeNodePath = path.join(runtimeRoot, runtimeNodeName);
+ fs.copyFileSync(bundledNode, runtimeNodePath);
+ try {
+ if (process.platform !== "win32") fs.chmodSync(runtimeNodePath, 0o755);
+ } catch (_) {}
+ }
+
return runtimeBridgeScript;
}
@@ -151,6 +236,10 @@ function log(line) {
logEl.scrollTop = logEl.scrollHeight;
}
+function statusIsRunning() {
+ return String(statusEl.textContent || "").toLowerCase().includes("running");
+}
+
function httpJson(url) {
return new Promise((resolve, reject) => {
const req = http.get(url, (res) => {
@@ -170,6 +259,51 @@ function httpJson(url) {
});
}
+async function isBridgeHealthy() {
+ try {
+ const health = await httpJson(`${BRIDGE_URL}/health`);
+ return Boolean(health && health.ok);
+ } catch (_) {
+ return false;
+ }
+}
+
+function stopHealthMonitor() {
+ if (healthMonitorTimer) {
+ clearInterval(healthMonitorTimer);
+ healthMonitorTimer = null;
+ }
+}
+
+function startHealthMonitor() {
+ stopHealthMonitor();
+ healthMonitorTimer = setInterval(async () => {
+ if (!bridgeProc) {
+ stopHealthMonitor();
+ return;
+ }
+ const healthy = await isBridgeHealthy();
+ if (healthy) {
+ setStatus("running");
+ return;
+ }
+ if (!statusIsRunning()) {
+ setStatus("starting...");
+ }
+ }, 3000);
+}
+
+async function waitForBridgeReady(maxWaitMs = 12000) {
+ const startAt = Date.now();
+ while (Date.now() - startAt < maxWaitMs) {
+ if (!bridgeProc) return false;
+ const healthy = await isBridgeHealthy();
+ if (healthy) return true;
+ await new Promise((resolve) => setTimeout(resolve, 300));
+ }
+ return false;
+}
+
function startBridge() {
if (bridgeProc) {
log("Bridge already running in this app session.");
@@ -201,20 +335,39 @@ function startBridge() {
windowsHide: false
});
setStatus("starting...");
- bridgeProc.stdout.on("data", (chunk) => log(String(chunk).trimEnd()));
+ bridgeProc.stdout.on("data", (chunk) => {
+ const text = String(chunk || "").trimEnd();
+ log(text);
+ if (/Listening on http:\/\/127\.0\.0\.1:\d+/i.test(text)) {
+ setStatus("running");
+ }
+ });
bridgeProc.stderr.on("data", (chunk) => log(String(chunk).trimEnd()));
bridgeProc.on("error", (err) => {
+ stopHealthMonitor();
log(`Bridge launch failed: ${err && err.message ? err.message : String(err)}`);
bridgeProc = null;
setStatus("failed");
});
bridgeProc.on("exit", (code) => {
+ stopHealthMonitor();
log(`Bridge exited with code ${code}`);
bridgeProc = null;
setStatus("stopped");
});
log("Bridge process started.");
+ startHealthMonitor();
+ waitForBridgeReady().then((ready) => {
+ if (!bridgeProc) return;
+ if (ready) {
+ setStatus("running");
+ return;
+ }
+ setStatus("starting...");
+ log("Bridge did not report healthy within 12s. Use Check Health for details.");
+ });
} catch (err) {
+ stopHealthMonitor();
log(`Start handler error: ${err && err.message ? err.message : String(err)}`);
setStatus("failed");
}
@@ -227,6 +380,7 @@ function stopBridge() {
}
try {
bridgeProc.kill();
+ stopHealthMonitor();
log("Sent stop signal to bridge process.");
} catch (err) {
log(`Failed to stop bridge: ${err.message}`);
@@ -237,6 +391,12 @@ async function checkHealth() {
try {
const health = await httpJson(`${BRIDGE_URL}/health`);
setStatus(health && health.ok ? "running" : "degraded");
+ if (health && (typeof health.nerdModeEnabled === "boolean" || health.updateIntervalMs != null)) {
+ applyBridgeSettingsToUi({
+ nerdModeEnabled: health.nerdModeEnabled,
+ updateIntervalMs: health.updateIntervalMs
+ });
+ }
log(`Health: ${JSON.stringify(health)}`);
} catch (err) {
setStatus("offline");
@@ -262,8 +422,28 @@ startBtn.addEventListener("click", startBridge);
stopBtn.addEventListener("click", stopBridge);
healthBtn.addEventListener("click", checkHealth);
probeBtn.addEventListener("click", probeNowPlaying);
+if (nerdModeToggleEl) {
+ nerdModeToggleEl.addEventListener("change", () => {
+ const settings = saveBridgeSettings({
+ nerdModeEnabled: Boolean(nerdModeToggleEl.checked),
+ updateIntervalMs: updateIntervalInputEl ? updateIntervalInputEl.value : DEFAULT_UPDATE_INTERVAL_MS
+ });
+ log(`Nerd Mode ${settings.nerdModeEnabled ? "enabled" : "disabled"}.`);
+ });
+}
+if (updateIntervalInputEl) {
+ updateIntervalInputEl.addEventListener("change", () => {
+ const settings = saveBridgeSettings({
+ nerdModeEnabled: nerdModeToggleEl ? Boolean(nerdModeToggleEl.checked) : false,
+ updateIntervalMs: updateIntervalInputEl.value
+ });
+ applyBridgeSettingsToUi(settings);
+ log(`Update interval set to ${settings.updateIntervalMs}ms.`);
+ });
+}
window.addEventListener("beforeunload", () => {
+ stopHealthMonitor();
if (bridgeProc) {
try {
bridgeProc.kill();
@@ -356,5 +536,6 @@ function setupTrayBehavior() {
}
setStatus("idle");
+applyBridgeSettingsToUi(loadBridgeSettings());
log("Bridge NW app ready.");
setupTrayBehavior();
diff --git a/bridge-nw/index.html b/bridge-nw/index.html
index a30edb8..d62e74c 100644
--- a/bridge-nw/index.html
+++ b/bridge-nw/index.html
@@ -11,6 +11,10 @@
button { background: #1f2430; color: #e7e9ee; border: 1px solid #2e3544; border-radius: 8px; padding: 8px 12px; cursor: pointer; }
button:hover { background: #2a3142; }
.status { background: #121622; border: 1px solid #2e3544; border-radius: 8px; padding: 10px; margin-bottom: 12px; }
+ .toggleRow { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; padding: 10px 12px; background: #121622; border: 1px solid #2e3544; border-radius: 8px; }
+ .toggleRow label { display: flex; align-items: center; gap: 8px; cursor: pointer; }
+ .toggleHint { color: #9aa4b4; font-size: 12px; }
+ .settingInput { background: #0b0d14; color: #e7e9ee; border: 1px solid #2e3544; border-radius: 6px; padding: 6px 8px; width: 120px; }
pre { background: #0b0d14; border: 1px solid #2e3544; border-radius: 8px; padding: 12px; height: 460px; overflow: auto; white-space: pre-wrap; }
code { color: #8ad1ff; }
@@ -24,6 +28,18 @@
BetterFluxer Bridge (NW.js)
+
+
+ When nothing is playing, publish system stats instead.
+
+
+
+
+ Suggested poll interval for status sync.
+
Status: idle
diff --git a/bridge-nw/scripts/local-bridge.js b/bridge-nw/scripts/local-bridge.js
index bdb6d07..94a04f4 100644
--- a/bridge-nw/scripts/local-bridge.js
+++ b/bridge-nw/scripts/local-bridge.js
@@ -31,13 +31,20 @@ const BRIDGE_BASE_DIR = getBridgeBaseDir();
const DATA_DIR = path.join(BRIDGE_BASE_DIR, "data");
const TOKEN_FILE = path.join(DATA_DIR, "bridge-token.txt");
const CACHE_FILE = path.join(DATA_DIR, "bridge-cache.json");
+const SETTINGS_FILE = path.join(DATA_DIR, "bridge-settings.json");
const HOST = "127.0.0.1";
const PORT = Number.parseInt(process.env.BF_BRIDGE_PORT || "21864", 10);
-const BRIDGE_VERSION = "2026-03-07-rpc-tuna";
+const BRIDGE_VERSION_BY_OS = {
+ win32: "2026-03-10-rpc-win",
+ linux: "2026-03-10-rpc-linux",
+ darwin: "2026-03-10-rpc-mac"
+};
+const BRIDGE_VERSION = BRIDGE_VERSION_BY_OS[process.platform] || "2026-03-10-rpc";
const REQUEST_TIMEOUT_MS = Number.parseInt(process.env.BF_BRIDGE_TIMEOUT_MS || "12000", 10);
const DEFAULT_TTL_SECONDS = Number.parseInt(process.env.BF_BRIDGE_DEFAULT_TTL || "120", 10);
const MAX_TTL_SECONDS = Number.parseInt(process.env.BF_BRIDGE_MAX_TTL || "1800", 10);
+const DEFAULT_UPDATE_INTERVAL_MS = Number.parseInt(process.env.BF_BRIDGE_UPDATE_INTERVAL_MS || "10000", 10);
const WINDOWS_RUN_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
const WINDOWS_RUN_VALUE = "BetterFluxerBridge";
const LINUX_BIN_DIR = path.join(os.homedir(), ".local", "bin");
@@ -111,6 +118,12 @@ function normalizeTtlSeconds(input) {
return Math.max(1, Math.min(MAX_TTL_SECONDS, n));
}
+function normalizeUpdateIntervalMs(input) {
+ const n = Number.parseInt(String(input || ""), 10);
+ if (!Number.isFinite(n) || n <= 0) return DEFAULT_UPDATE_INTERVAL_MS;
+ return Math.max(1000, Math.min(60000, Math.round(n)));
+}
+
function readJsonBody(req) {
return new Promise((resolve) => {
let raw = "";
@@ -148,6 +161,21 @@ function saveCache(cache) {
fs.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2), "utf8");
}
+function loadBridgeSettings() {
+ if (!fs.existsSync(SETTINGS_FILE)) {
+ return { nerdModeEnabled: false, updateIntervalMs: DEFAULT_UPDATE_INTERVAL_MS };
+ }
+ try {
+ const parsed = JSON.parse(fs.readFileSync(SETTINGS_FILE, "utf8"));
+ return {
+ nerdModeEnabled: Boolean(parsed && parsed.nerdModeEnabled),
+ updateIntervalMs: normalizeUpdateIntervalMs(parsed && parsed.updateIntervalMs)
+ };
+ } catch (_) {
+ return { nerdModeEnabled: false, updateIntervalMs: DEFAULT_UPDATE_INTERVAL_MS };
+ }
+}
+
function parseArgv(argv) {
const out = {};
for (const item of argv || []) {
@@ -387,6 +415,41 @@ function normalizeNowPlayingPayload(payload, source) {
};
}
+function formatDurationShort(totalSeconds) {
+ const sec = Math.max(0, Math.floor(Number(totalSeconds) || 0));
+ const hours = Math.floor(sec / 3600);
+ const minutes = Math.floor((sec % 3600) / 60);
+ const seconds = sec % 60;
+ if (hours > 0) return `${hours}h ${minutes}m`;
+ if (minutes > 0) return `${minutes}m ${seconds}s`;
+ return `${seconds}s`;
+}
+
+function buildNerdModeNowPlaying() {
+ const totalMem = os.totalmem();
+ const freeMem = os.freemem();
+ const usedMem = Math.max(0, totalMem - freeMem);
+ const memPercent = totalMem > 0 ? Math.round((usedMem / totalMem) * 100) : 0;
+ const loadAvg = os.loadavg();
+ const cpuCount = Array.isArray(os.cpus()) ? os.cpus().length : 0;
+ const hostname = String(os.hostname() || "").trim();
+ const details = `CPU ${Number(loadAvg[0] || 0).toFixed(2)} | RAM ${memPercent}%`;
+ const state = `${hostname || process.platform} | up ${formatDurationShort(os.uptime())}`;
+ return normalizeNowPlayingPayload(
+ {
+ source: "nerd-mode",
+ kind: "system",
+ title: "Nerd Mode",
+ details,
+ state,
+ appId: hostname || process.platform,
+ playbackStatus: "idle",
+ name: `System Stats (${cpuCount} cores)`
+ },
+ "nerd-mode"
+ );
+}
+
function makeRpcFrame(op, payloadObject) {
const payload = Buffer.from(JSON.stringify(payloadObject || {}), "utf8");
const frame = Buffer.allocUnsafe(8 + payload.length);
@@ -418,6 +481,28 @@ function decodeRpcFrames(buffer, onFrame) {
return buffer.slice(cursor);
}
+function isIgnorableSocketWriteError(error) {
+ const code = String((error && error.code) || "");
+ return code === "EPIPE" || code === "ECONNRESET" || code === "ERR_STREAM_DESTROYED";
+}
+
+function safeSocketWrite(socket, frame) {
+ if (!socket || socket.destroyed) return false;
+ try {
+ socket.write(frame, (error) => {
+ if (error && !isIgnorableSocketWriteError(error)) {
+ console.warn("[BetterFluxer Bridge] RPC socket write error:", String(error.message || error));
+ }
+ });
+ return true;
+ } catch (error) {
+ if (!isIgnorableSocketWriteError(error)) {
+ console.warn("[BetterFluxer Bridge] RPC socket write failed:", String((error && error.message) || error || "unknown"));
+ }
+ return false;
+ }
+}
+
function sendJson(res, status, payload, origin) {
if (origin) res.setHeader("Access-Control-Allow-Origin", origin);
res.setHeader("Access-Control-Allow-Headers", "Content-Type,Authorization,X-BetterFluxer-Token");
@@ -553,7 +638,7 @@ function handleDiscordRpcMessage(message, state, socket) {
state.lastRpcActivityAt = Date.now();
console.log(`[BetterFluxer Bridge] RPC activity captured: ${formatNowPlayingForLog(payload)}`);
if (socket && !socket.destroyed) {
- socket.write(makeRpcFrame(1, { cmd: "SET_ACTIVITY", data: {}, evt: null, nonce }));
+ safeSocketWrite(socket, makeRpcFrame(1, { cmd: "SET_ACTIVITY", data: {}, evt: null, nonce }));
}
return;
}
@@ -562,32 +647,116 @@ function handleDiscordRpcMessage(message, state, socket) {
state.lastRpcActivity = null;
state.lastRpcActivityAt = Date.now();
if (socket && !socket.destroyed) {
- socket.write(makeRpcFrame(1, { cmd: "CLEAR_ACTIVITY", data: {}, evt: null, nonce }));
+ safeSocketWrite(socket, makeRpcFrame(1, { cmd: "CLEAR_ACTIVITY", data: {}, evt: null, nonce }));
}
return;
}
if (socket && !socket.destroyed && nonce != null && cmd) {
- socket.write(makeRpcFrame(1, { cmd, data: {}, evt: null, nonce }));
+ safeSocketWrite(socket, makeRpcFrame(1, { cmd, data: {}, evt: null, nonce }));
+ }
+}
+
+function getLinuxDiscordIpcDirs() {
+ const dirs = [];
+ const pushDir = (value) => {
+ const v = String(value || "").trim();
+ if (!v) return;
+ if (!dirs.includes(v)) dirs.push(v);
+ };
+
+ pushDir(process.env.BF_RPC_DIR);
+ pushDir(process.env.XDG_RUNTIME_DIR);
+ if (Number.isFinite(Number(process.getuid && process.getuid()))) {
+ pushDir(path.join("/run/user", String(process.getuid())));
+ }
+ pushDir("/tmp");
+ pushDir("/var/tmp");
+ pushDir("/dev/shm");
+
+ const expanded = [];
+ const suffixes = [
+ "",
+ "app/com.discordapp.Discord",
+ "app/com.discordapp.DiscordCanary",
+ "app/com.discordapp.DiscordPTB",
+ "app/com.vesktop.Vesktop",
+ "app/dev.vencord.Vesktop",
+ "snap.discord"
+ ];
+ for (const dir of dirs) {
+ for (const suffix of suffixes) {
+ const full = suffix ? path.join(dir, suffix) : dir;
+ if (!expanded.includes(full)) expanded.push(full);
+ }
+ }
+ return expanded;
+}
+
+function getDiscordRpcEndpoints() {
+ if (process.platform === "win32") {
+ return Array.from({ length: 10 }, (_, i) => `\\\\.\\pipe\\discord-ipc-${i}`);
+ }
+ if (process.platform === "linux") {
+ const out = [];
+ for (const base of getLinuxDiscordIpcDirs()) {
+ for (let i = 0; i < 10; i += 1) {
+ out.push(path.join(base, `discord-ipc-${i}`));
+ }
+ }
+ return out;
+ }
+ return [];
+}
+
+function ensureLinuxRpcSocketPath(socketPath) {
+ const dir = path.dirname(socketPath);
+ if (!fs.existsSync(dir)) {
+ return false;
}
+ if (!fs.existsSync(socketPath)) return true;
+ try {
+ const stat = fs.lstatSync(socketPath);
+ if (stat.isSocket()) {
+ fs.unlinkSync(socketPath);
+ return true;
+ }
+ } catch (_) {}
+ return !fs.existsSync(socketPath);
}
function startDiscordRpcCapture(state) {
- if (process.platform !== "win32") return;
+ if (process.platform !== "win32" && process.platform !== "linux") return;
if (state.rpcServers && state.rpcServers.length) return;
state.rpcServers = [];
state.rpcBindErrors = [];
- for (let i = 0; i < 10; i += 1) {
- const pipeName = `\\\\.\\pipe\\discord-ipc-${i}`;
+ const endpoints = getDiscordRpcEndpoints();
+ for (const endpoint of endpoints) {
+ if (process.platform === "linux") {
+ try {
+ const ready = ensureLinuxRpcSocketPath(endpoint);
+ if (!ready) continue;
+ } catch (error) {
+ state.rpcBindErrors.push({ pipe: endpoint, error: String((error && error.message) || error || "unknown") });
+ continue;
+ }
+ }
+
const server = net.createServer((socket) => {
let buf = Buffer.alloc(0);
+ socket.on("error", (error) => {
+ if (!isIgnorableSocketWriteError(error)) {
+ console.warn("[BetterFluxer Bridge] RPC socket error:", String((error && error.message) || error || "unknown"));
+ }
+ });
socket.on("data", (chunk) => {
try {
buf = Buffer.concat([buf, Buffer.from(chunk)]);
buf = decodeRpcFrames(buf, (op, msg) => {
if (op === 0) {
- socket.write(
+ safeSocketWrite(
+ socket,
makeRpcFrame(1, {
cmd: "DISPATCH",
evt: "READY",
@@ -612,53 +781,24 @@ function startDiscordRpcCapture(state) {
});
server.on("error", (error) => {
- state.rpcBindErrors.push({ pipe: pipeName, error: String((error && error.message) || error || "unknown") });
+ state.rpcBindErrors.push({ pipe: endpoint, error: String((error && error.message) || error || "unknown") });
});
try {
- server.listen(pipeName, () => {
- state.rpcServers.push({ pipe: pipeName, server });
+ server.listen(endpoint, () => {
+ if (process.platform === "linux") {
+ try {
+ fs.chmodSync(endpoint, 0o600);
+ } catch (_) {}
+ }
+ state.rpcServers.push({ pipe: endpoint, server });
});
} catch (error) {
- state.rpcBindErrors.push({ pipe: pipeName, error: String((error && error.message) || error || "unknown") });
+ state.rpcBindErrors.push({ pipe: endpoint, error: String((error && error.message) || error || "unknown") });
}
}
}
-async function queryLinuxMedia() {
- if (process.platform !== "linux") {
- return { ok: false, error: "Linux only", platform: process.platform };
- }
-
- const format = "{{title}}\t{{artist}}\t{{album}}\t{{playerName}}\t{{status}}";
- try {
- const result = await runCommand("playerctl", ["metadata", "--format", format], 4000);
- if (result.code !== 0) {
- const errText = (result.stderr || result.stdout || "").trim();
- if (/No players found/i.test(errText)) {
- return { ok: true, hasSession: false, source: "linux-mpris" };
- }
- return { ok: false, error: errText || `playerctl exited ${result.code}` };
- }
-
- const line = String(result.stdout || "").trim();
- if (!line) return { ok: true, hasSession: false, source: "linux-mpris" };
- const [title = "", artist = "", albumTitle = "", appId = "", playbackStatus = ""] = line.split("\t");
- return {
- ok: true,
- hasSession: Boolean(String(title).trim() || String(artist).trim()),
- source: "linux-mpris",
- title: String(title || "").trim(),
- artist: String(artist || "").trim(),
- albumTitle: String(albumTitle || "").trim(),
- appId: String(appId || "").trim(),
- playbackStatus: String(playbackStatus || "").trim()
- };
- } catch (error) {
- return { ok: false, error: String((error && error.message) || error || "playerctl failed") };
- }
-}
-
async function queryMacMedia() {
if (process.platform !== "darwin") {
return { ok: false, error: "macOS only", platform: process.platform };
@@ -727,6 +867,7 @@ async function queryMacMedia() {
async function queryUniversalNowPlaying(state) {
const now = Date.now();
+ let rpcFallback = null;
if (state && state.lastRpcActivity && now - Number(state.lastRpcActivityAt || 0) < 180000) {
const raw = state.lastRpcActivity.raw && typeof state.lastRpcActivity.raw === "object" ? state.lastRpcActivity.raw : {};
const base = state.lastRpcActivity.normalized && typeof state.lastRpcActivity.normalized === "object"
@@ -741,31 +882,49 @@ async function queryUniversalNowPlaying(state) {
if (startMs != null && endMs != null && endMs > startMs) {
base.durationMs = endMs - startMs;
}
- return base;
+ if (base && base.ok && base.hasSession) {
+ return base;
+ }
+ rpcFallback = {
+ ok: true,
+ hasSession: false,
+ source: String(base.source || "discord-rpc-pipe"),
+ error: String(base.error || "No active RPC session")
+ };
}
+ let fallback = null;
if (process.platform === "win32") {
const tuna = await queryTunaNowPlaying(state);
if (tuna && tuna.ok && tuna.hasSession) return tuna;
- return tuna && tuna.ok
+ fallback = tuna && tuna.ok
? tuna
: { ok: false, hasSession: false, source: "windows-now-playing", error: "No active RPC or Tuna session" };
- }
- if (process.platform === "linux") {
- const linux = await queryLinuxMedia();
- if (linux.ok && linux.hasSession) return linux;
+ } else if (process.platform === "linux") {
const tuna = await queryTunaNowPlaying(state);
if (tuna && tuna.ok && tuna.hasSession) return tuna;
- return linux.ok ? linux : tuna.ok ? tuna : linux;
- }
- if (process.platform === "darwin") {
+ fallback = tuna && tuna.ok
+ ? tuna
+ : { ok: false, hasSession: false, source: "linux-now-playing", error: "No active Tuna session" };
+ } else if (process.platform === "darwin") {
const mac = await queryMacMedia();
if (mac.ok && mac.hasSession) return mac;
const tuna = await queryTunaNowPlaying(state);
if (tuna && tuna.ok && tuna.hasSession) return tuna;
- return mac.ok ? mac : tuna.ok ? tuna : mac;
+ fallback = mac.ok ? mac : tuna.ok ? tuna : mac;
+ } else {
+ fallback = { ok: false, error: `Unsupported platform: ${process.platform}` };
+ }
+
+ const preferredFallback =
+ fallback && fallback.hasSession
+ ? fallback
+ : rpcFallback || fallback;
+
+ if (loadBridgeSettings().nerdModeEnabled && (!preferredFallback || !preferredFallback.hasSession)) {
+ return buildNerdModeNowPlaying();
}
- return { ok: false, error: `Unsupported platform: ${process.platform}` };
+ return preferredFallback;
}
function formatNowPlayingForLog(np) {
@@ -906,6 +1065,7 @@ async function main() {
const url = new URL(req.url || "/", `http://${HOST}:${PORT}`);
if (url.pathname === "/health") {
+ const settings = loadBridgeSettings();
return sendJson(
res,
200,
@@ -917,6 +1077,8 @@ async function main() {
rpcPipesListening: (bridgeState.rpcServers || []).length,
rpcPipeErrors: (bridgeState.rpcBindErrors || []).length,
tunaPath: bridgeState.tunaPath,
+ nerdModeEnabled: Boolean(settings.nerdModeEnabled),
+ updateIntervalMs: settings.updateIntervalMs,
allowlist,
uptimeSec: Math.floor(process.uptime())
},
diff --git a/do_not_edit/fluxer b/do_not_edit/fluxer
new file mode 160000
index 0000000..03813bb
--- /dev/null
+++ b/do_not_edit/fluxer
@@ -0,0 +1 @@
+Subproject commit 03813bbe17db008452f0f1be3090a7d2970a5447
diff --git a/nw/index.html b/nw/index.html
index 5bc695c..a2030bc 100644
--- a/nw/index.html
+++ b/nw/index.html
@@ -71,7 +71,7 @@
Injecting
-