Add a soprano recorder tab converter as a Linux AppImage.

The app turns a lead line into Baroque fingering tabs with rhythm kept, ships Twinkle and Ode to Joy, and reads photos locally with oemer when the bundled Python is present.

Co-authored-by: catpersonisme <catpersonisme@gmail.com>
This commit is contained in:
Cursor Agentandchen committed 2026-10-09 03:35:16 +00:00
1 parent 72c2f509f6
commit 59d65333e3
55 files changed
+17536

No files matched your search

+126
View File
@@ -0,0 +1,126 @@
const { app, BrowserWindow, dialog } = require("electron");
const { spawn } = require("node:child_process");
const fs = require("node:fs");
const http = require("node:http");
const net = require("node:net");
const path = require("node:path");
app.commandLine.appendSwitch("no-sandbox");
app.commandLine.appendSwitch("disable-dev-shm-usage");
let server;
function freePort() {
return new Promise((resolve, reject) => {
const probe = net.createServer();
probe.listen(0, "127.0.0.1", () => {
const address = probe.address();
const port = address && typeof address === "object" ? address.port : 0;
probe.close(() => resolve(port));
});
probe.on("error", reject);
});
}
function waitFor(url, timeoutMs) {
const started = Date.now();
return new Promise((resolve, reject) => {
const tick = () => {
const request = http.get(url, (response) => {
response.resume();
if (response.statusCode && response.statusCode < 500) resolve();
else retry();
});
request.on("error", retry);
};
const retry = () => {
if (Date.now() - started > timeoutMs) reject(new Error(`Timed out waiting for ${url}`));
else setTimeout(tick, 250);
};
tick();
});
}
function stopServer() {
if (server && !server.killed) server.kill("SIGTERM");
}
async function open() {
const resources = process.resourcesPath;
const standalone = path.join(resources, "standalone");
const python = path.join(resources, "python", "bin", "python3");
const serverJs = path.join(standalone, "server.js");
if (!fs.existsSync(serverJs) || !fs.existsSync(python)) {
dialog.showErrorBox(
"Recorder Tab Converter",
"The bundled app or the local reader is missing from this AppImage.",
);
app.quit();
return;
}
const port = await freePort();
const logPath = path.join(app.getPath("temp"), "recorder-tab-converter.log");
const log = fs.createWriteStream(logPath, { flags: "a" });
server = spawn(process.execPath, [serverJs], {
cwd: standalone,
env: {
...process.env,
ELECTRON_RUN_AS_NODE: "1",
PORT: String(port),
HOSTNAME: "127.0.0.1",
OEMER_PYTHON: python,
NODE_ENV: "production",
},
stdio: ["ignore", "pipe", "pipe"],
});
server.stdout.pipe(log);
server.stderr.pipe(log);
server.on("exit", (code) => {
log.write(`\nserver exit ${code}\n`);
});
const url = `http://127.0.0.1:${port}/`;
try {
await waitFor(url, 45_000);
} catch (error) {
dialog.showErrorBox(
"Recorder Tab Converter",
`${error instanceof Error ? error.message : "The app server did not start."}\nLog: ${logPath}`,
);
stopServer();
app.quit();
return;
}
const window = new BrowserWindow({
width: 1280,
height: 880,
minWidth: 390,
minHeight: 640,
show: false,
title: "Recorder Tab Converter",
backgroundColor: "#f3ecdf",
autoHideMenuBar: true,
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
// Renderer sandbox cannot create shared memory here, so the window
// fails with ERR_FAILED. The page is the local UI and has no Node access.
sandbox: false,
},
});
window.once("ready-to-show", () => window.show());
await window.loadURL(url);
}
app.whenReady().then(open).catch((error) => {
dialog.showErrorBox("Recorder Tab Converter", error instanceof Error ? error.message : String(error));
app.quit();
});
app.on("before-quit", stopServer);
app.on("window-all-closed", () => {
stopServer();
app.quit();
});