import { readdir, readFile } from "node:fs/promises"; import { pathToFileURL } from "node:url"; import path from "node:path"; import { sessionBus, listPlayers, selectPlayer, getPlayerData, watchPlayer, watchBus, } from "./mpris.js"; import { formatTrackData } from "./formatter.js"; const modulesDir = path.resolve(import.meta.dirname, "modules"); const configDir = path.resolve(import.meta.dirname, "../config"); let modules = []; let globalConfig = {}; async function loadGlobalConfig() { const configPath = path.join(configDir, "global.json"); try { globalConfig = JSON.parse(await readFile(configPath, "utf-8")); } catch { globalConfig = {}; } } async function loadConfig(moduleName) { const baseName = moduleName.replace(/\.js$/, ""); const configPath = path.join(configDir, `${baseName}.json`); try { return JSON.parse(await readFile(configPath, "utf-8")); } catch { return null; } } async function loadModules() { const files = await readdir(modulesDir); const jsFiles = files.filter((f) => f.endsWith(".js")); modules = []; for (const file of jsFiles) { const url = pathToFileURL(path.join(modulesDir, file)).href; try { const mod = await import(url); if (typeof mod.onData !== "function") { console.warn(`[pipris] Skipping ${file} (no onData export)`); continue; } const config = await loadConfig(file); if (typeof mod.init === "function") { await mod.init(config); } modules.push({ name: file, onData: mod.onData, onClear: typeof mod.onClear === "function" ? mod.onClear : null, }); console.log(`[pipris] Loaded module: ${file}`); } catch (err) { console.error(`[pipris] Failed to load ${file}: ${err.message}`); } } } async function callModules(data) { for (const mod of modules) { try { await mod.onData(data); } catch (err) { console.error(`[pipris] Module ${mod.name} error: ${err.message}`); } } } async function callModulesClear() { for (const mod of modules) { if (!mod.onClear) continue; try { await mod.onClear(); } catch (err) { console.error(`[pipris] Module ${mod.name} onClear error: ${err.message}`); } } } let bus; let activePlayer = null; let cleanupWatch = null; let currentTrackId = null; let currentPlayedTime = null; async function emitCurrentState(overridePositionUs, seeked = false) { if (!activePlayer) return; try { const { metadata, playbackStatus, positionUs } = await getPlayerData( bus, activePlayer, ); const trackId = metaTrackId(metadata); if (trackId !== currentTrackId) { currentTrackId = trackId; currentPlayedTime = new Date().toISOString(); } const data = formatTrackData( metadata, playbackStatus, overridePositionUs ?? positionUs, currentPlayedTime, ); if (data) { data.seeked = seeked; callModules(data); } } catch (err) { console.error(`[pipris] Failed to read player data: ${err.message}`); } } function metaTrackId(metadata) { const id = metadata["mpris:trackid"]; if (id) return String(id.value); const title = metadata["xesam:title"]; const url = metadata["xesam:url"]; return `${title?.value ?? ""}|${url?.value ?? ""}`; } async function attachToPlayer(playerName) { if (cleanupWatch) { cleanupWatch(); cleanupWatch = null; } activePlayer = playerName; console.log(`[pipris] Attached to ${playerName}`); await emitCurrentState(); cleanupWatch = await watchPlayer( bus, playerName, async (changed) => { if ( changed.PlaybackStatus !== undefined || changed.Metadata !== undefined ) { await emitCurrentState(); } }, async (positionUs) => { await emitCurrentState(positionUs, true); }, ); } async function scan() { const players = await listPlayers(bus); if (players.length === 0) { if (activePlayer) { console.log("[pipris] No MPRIS players found. Waiting..."); if (cleanupWatch) { cleanupWatch(); cleanupWatch = null; } activePlayer = null; currentTrackId = null; currentPlayedTime = null; await callModulesClear(); } return; } const best = selectPlayer(players, globalConfig.playerListMode); if (!best) { if (activePlayer) { console.log("[pipris] No whitelisted players found. Waiting..."); if (cleanupWatch) { cleanupWatch(); cleanupWatch = null; } activePlayer = null; currentTrackId = null; currentPlayedTime = null; await callModulesClear(); } return; } // Only re-attach if the selected player changed if (best !== activePlayer) { try { await attachToPlayer(best); } catch (err) { console.error( `[pipris] Failed to attach to ${best}: ${err.message}`, ); activePlayer = null; } } } async function main() { await loadGlobalConfig(); await loadModules(); bus = sessionBus(); // Watch for players appearing / disappearing await watchBus(bus, () => { scan().catch((err) => console.error(`[pipris] Scan error: ${err.message}`), ); }); // Initial scan await scan(); if (!activePlayer) { console.log("[pipris] No MPRIS players found. Waiting for one to start..."); } } main().catch((err) => { console.error(`[pipris] Fatal: ${err.message}`); process.exit(1); });