// ==UserScript==
// @name Multiplayer Piano Optimizations [Input/Output]
// @namespace https://tampermonkey.net/
// @version 1.0.2
// @description Saves and persists MIDI input/output options for you
// @author zackiboiz
// @match *://*.multiplayerpiano.com/*
// @match *://*.multiplayerpiano.net/*
// @match *://dev.multiplayerpiano.net/*
// @match *://*.multiplayerpiano.org/*
// @match *://*.multiplayerpiano.dev/*
// @match *://piano.mpp.community/*
// @match *://mpp.7458.space/*
// @match *://qmppv2.qwerty0301.repl.co/*
// @match *://mpp.8448.space/*
// @match *://mpp.autoplayer.xyz/*
// @match *://mpp.hyye.xyz/*
// @match *://mpp.smp-meow.net/*
// @match *://piano.ourworldofpixels.com/*
// @match *://mpp.lapishusky.dev/*
// @match *://staging-mpp.sad.ovh/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=multiplayerpiano.net
// @grant GM_info
// @license MIT
// @run-at document-start
// ==/UserScript==
(async () => {
const dl = GM_info.script.downloadURL || GM_info.script.updateURL || GM_info.script.homepageURL || "";
const match = dl.match(/greasyfork\.org\/scripts\/(\d+)/);
if (!match) {
console.warn("Could not find Greasy Fork script ID in downloadURL/updateURL/homepageURL:", dl);
} else {
const scriptId = match[1];
const localVersion = GM_info.script.version;
const apiUrl = `https://greatest.deepsurf.us/scripts/${scriptId}.json`;
fetch(apiUrl, {
mode: "cors",
headers: {
Accept: "application/json"
}
}).then(r => {
if (!r.ok) throw new Error("Failed to fetch Greasy Fork data.");
return r.json();
}).then(data => {
const remoteVersion = data.version;
if (compareVersions(localVersion, remoteVersion) < 0) {
new MPP.Notification({
"m": "notification",
"duration": 15000,
"title": "Update Available",
"html": "<p>A new version of this script is available!</p>" +
`<p style="margin-top: 10px;">Script: ${GM_info.script.name}</p>` +
`<p>Local: v${localVersion}</p>` +
`<p>Latest: v${remoteVersion}</p>` +
`<a href="https://greatest.deepsurf.us/scripts/${scriptId}" target="_blank" style="position: absolute; right: 0;bottom: 0; margin: 10px; font-size: 0.5rem;">Open Greasy Fork to update?</a>`
})
}
}).catch(err => console.error("Update check failed:", err));
}
function compareVersions(a, b) {
const pa = a.split(".").map(n => parseInt(n, 10) || 0);
const pb = b.split(".").map(n => parseInt(n, 10) || 0);
const len = Math.max(pa.length, pb.length);
for (let i = 0; i < len; i++) {
if ((pa[i] || 0) < (pb[i] || 0)) return -1;
if ((pa[i] || 0) > (pb[i] || 0)) return 1;
}
return 0;
}
const STORAGE_KEY = "midiConnections";
const SAVE_DEBOUNCE_MS = 40;
function loadMap() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
return raw ? JSON.parse(raw) : {};
} catch (e) {
console.warn("MIDI persist load failed", e);
return {};
}
}
function saveMap(map) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(map));
} catch (e) {
console.warn("MIDI persist save failed", e);
}
}
function idKey(kind, id) {
return `${kind}:id:${(id || "").trim()}`;
}
function nameKey(kind, name) {
return `${kind}:name:${(name || "").trim()}`;
}
function tn(s) {
return (s || "").trim();
}
function getSavedForDevice(map, kind, device) {
if (!device) return undefined;
if (device.id) {
const k = idKey(kind, device.id);
if (Object.prototype.hasOwnProperty.call(map, k)) return !!map[k];
}
const nk = nameKey(kind, device.name || "");
if (Object.prototype.hasOwnProperty.call(map, nk)) return !!map[nk];
return undefined;
}
function setSavedForDevice(map, kind, device, enabled) {
if (device && device.id) map[idKey(kind, device.id)] = !!enabled;
if (device && device.name) map[nameKey(kind, device.name)] = !!enabled;
}
function applySavedStates(midi) {
if (!midi) return;
const map = loadMap();
// inputs
for (let it = midi.inputs.values(), n = it.next(); n && !n.done; n = it.next()) {
const input = n.value;
const saved = getSavedForDevice(map, "input", input);
if (typeof saved !== "undefined") {
try {
input.enabled = !!saved;
} catch (e) { /* ignore */ }
}
}
// outputs
for (let it = midi.outputs.values(), n = it.next(); n && !n.done; n = it.next()) {
const output = n.value;
const saved = getSavedForDevice(map, "output", output);
if (typeof saved !== "undefined") {
try {
output.enabled = !!saved;
} catch (e) { /* ignore */ }
}
}
}
function findDevice(midi, identifier, preferredKind) {
if (!midi || !identifier) return null;
const t = tn(identifier);
function tryFindById(kind) {
const list = kind === "input" ? midi.inputs.values() : midi.outputs.values();
for (let it = list, r = it.next(); r && !r.done; r = it.next()) {
const dev = r.value;
if (dev.id && dev.id === identifier) return { dev, kind };
}
return null;
}
function tryFindByName(kind) {
const list = kind === "input" ? midi.inputs.values() : midi.outputs.values();
for (let it = list, r = it.next(); r && !r.done; r = it.next()) {
const dev = r.value;
if (tn(dev.name) === t) return { dev, kind };
}
return null;
}
if (preferredKind === "output") {
return tryFindById("output") || tryFindByName("output") || tryFindById("input") || tryFindByName("input");
} else if (preferredKind === "input") {
return tryFindById("input") || tryFindByName("input") || tryFindById("output") || tryFindByName("output");
} else {
return tryFindById("input") || tryFindByName("input") || tryFindById("output") || tryFindByName("output");
}
}
function detectKindFromElement(el) {
if (!el) return null;
try {
let ancestor = el;
while (ancestor && ancestor !== document.body) {
if (ancestor.tagName && /^H\d$/i.test(ancestor.tagName) && /inputs?/i.test(ancestor.textContent || "")) return "input";
if (ancestor.tagName && /^H\d$/i.test(ancestor.tagName) && /outputs?/i.test(ancestor.textContent || "")) return "output";
ancestor = ancestor.parentElement;
}
const ul = el.closest("ul");
if (ul) {
let prev = ul.previousElementSibling;
while (prev) {
if (prev.tagName && /^H\d$/i.test(prev.tagName)) {
if (/inputs?/i.test(prev.textContent || "")) return "input";
if (/outputs?/i.test(prev.textContent || "")) return "output";
}
prev = prev.previousElementSibling;
}
}
} catch (e) { }
return null;
}
const lastSaved = new Map();
function shouldSaveKey(key, val) {
const now = Date.now();
const prev = lastSaved.get(key);
if (prev && prev.val === !!val && (now - prev.ts) < 1000) {
return false;
}
lastSaved.set(key, {
val: !!val,
ts: now
});
return true;
}
function persistDeviceKindState(kind, device, enabled) {
if (!kind || !device) return;
const map = loadMap();
if (device.id) {
const k = idKey(kind, device.id);
if (shouldSaveKey(k, enabled)) map[k] = !!enabled;
}
if (device.name) {
const kn = nameKey(kind, device.name);
if (shouldSaveKey(kn, enabled)) map[kn] = !!enabled;
}
saveMap(map);
}
function persistByName(kind, name, enabled) {
if (!name || !kind) return;
const map = loadMap();
const kn = nameKey(kind, name);
if (!shouldSaveKey(kn, enabled)) return;
map[kn] = !!enabled;
saveMap(map);
}
if (navigator.requestMIDIAccess) {
const orig = navigator.requestMIDIAccess.bind(navigator);
navigator.requestMIDIAccess = function (options) {
return orig(options).then((midi) => {
try {
applySavedStates(midi);
} catch (e) {
console.warn("MIDI persist apply error", e);
}
midi.addEventListener("statechange", () => {
setTimeout(() => {
try {
applySavedStates(midi);
} catch (e) { }
}, 60);
});
const processedElements = new WeakSet();
let saveTimer = null;
let pending = [];
function flushPending() {
if (!pending.length) return;
const copy = pending.slice();
pending = [];
for (const item of copy) {
const { el, displayName } = item;
const detectedKind = detectKindFromElement(el) || null;
const found = findDevice(midi, displayName, detectedKind || undefined);
if (found && found.dev) {
persistDeviceKindState(found.kind, found.dev, !!found.dev.enabled);
} else {
const kindToSave = detectedKind || "input";
const enabledFromClass = !!(el.classList && el.classList.contains("enabled"));
persistByName(kindToSave, displayName, enabledFromClass);
}
}
}
document.addEventListener("click", function (ev) {
const target = ev.target;
if (!target || typeof target.closest !== "function") return;
const li = target.closest(".connection");
if (!li) return;
if (processedElements.has(li)) {
// :hiiiperz:
} else {
processedElements.add(li);
setTimeout(() => processedElements.delete(li), 800);
}
const nameAttr = (li.getAttribute && (li.getAttribute("data-name") || li.getAttribute("title"))) || null;
const displayName = tn(nameAttr || li.textContent || "");
pending.push({ el: li, displayName });
if (saveTimer) clearTimeout(saveTimer);
saveTimer = setTimeout(() => { saveTimer = null; flushPending(); }, SAVE_DEBOUNCE_MS);
}, false);
const seen = new WeakSet();
const observer = new MutationObserver((mutations) => {
for (const m of mutations) {
if (!m.addedNodes || !m.addedNodes.length) continue;
for (const node of m.addedNodes) {
if (!node || node.nodeType !== 1) continue;
const elList = [];
if (node.classList && node.classList.contains("connection")) elList.push(node);
node.querySelectorAll && node.querySelectorAll(".connection").forEach(x => elList.push(x));
for (const el of elList) {
if (seen.has(el)) continue;
seen.add(el);
const nameAttr = (el.getAttribute && (el.getAttribute("data-name") || el.getAttribute("title"))) || null;
const displayName = tn(nameAttr || el.textContent || "");
const kind = detectKindFromElement(el);
if (!displayName) continue;
const found = findDevice(midi, displayName, kind || undefined);
const map = loadMap();
if (found && found.dev) {
const saved = getSavedForDevice(map, found.kind, found.dev);
if (typeof saved !== "undefined") {
try {
found.dev.enabled = !!saved;
} catch (e) { }
}
} else if (kind) {
const kn = nameKey(kind, displayName);
if (Object.prototype.hasOwnProperty.call(map, kn)) {
if (map[kn]) el.classList.add("enabled"); else el.classList.remove("enabled");
}
} else {
// :catkiss:
}
}
}
}
});
try {
const root = document.body || document.documentElement;
observer.observe(root, { childList: true, subtree: true });
} catch (e) { /* ignore */ }
return midi;
});
};
} else {
console.log("MIDI persist navigator.requestMIDIAccess not found");
}
})();