// Copyright 2014-2025 Jesper Larsson
//
// This file is part of Klipspringer, <https://klipspringer.avadeaux.net/>
//
// Klipspringer is free software: you can redistribute it and/or modify it under the terms of the
// GNU General Public License as published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// Klipspringer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
// even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with Klipspringer. If
// not, see <https://www.gnu.org/licenses/>.
import * as fs from 'fs';
import * as path from 'path';
import { createHttpServer, textFileService, imageFileService } from './http_services.js';
import { log } from './log.js';
import { networkInterfaces } from 'os';
import { setupNumpad } from './numpad.js';
import { setupRemote } from './remote.js';
import { setupSources } from './sources.js';
import { serverButtons, klipicon, kliplogo } from './svg_images.js';
import { defaultOptions, parseOptions, processOptions } from './parse_arguments.js'
process.on('unhandledRejection', (reason, promise) => {
log.error("Unhandled rejection: "+promise+" caused by: "+reason);
});
// -----------------------------------------------------------------------------
// Command-line and file options
const cliOpts = parseOptions(process.argv.slice(2));
const optionsFileName = path.join(cliOpts.etc || defaultOptions.etc, 'options.json');
const fileOpts = processOptions(fs.existsSync(optionsFileName)
? JSON.parse(fs.readFileSync(optionsFileName))
: { });
const opts = { ...defaultOptions, ...fileOpts, ...cliOpts };
opts.java_opts = [
"-cp", opts.classpath.map(e => path.resolve(path.join(...e.map(it => it[0] === '$' ? opts[it.slice(1)] : it)))).join(path.delimiter),
"--enable-native-access=ALL-UNNAMED",
"-Djava.library.path="+path.resolve(opts.lib)
];
if (opts.debug) {
opts.java_opts.push('-Dklipspringer.debug='+opts.debug);
} else {
log.debug = () => { }
}
const sources = setupSources(opts);
// -----------------------------------------------------------------------------
// The http interface
const clientDir = path.join(opts.lib, "client");
createHttpServer(clientDir, opts)
.addService('index.html', textFileService(() => 'sources.html', {
sources: sources.sourcesHtml,
kliplogo: kliplogo
}))
.addService('choice', (qpath, qstring, query, resp, host) => { sources.performChoice(query, resp, host) })
.addService('dir', (qpath, qstring, query, resp) =>
fs.promises.readdir(path.join(sources[query.source].path, query.path))
.then(files => { resp.passObj(files) })
.catch(err => { resp.httpError(err.toString()) }))
.addService('img', imageFileService(
(qpath, query) => query.file,
(qpath, query) => query.source ? path.join(sources[query.source].path, query.path || '', 'data') : path.join(clientDir, "img")))
.addService('favicon.ico', (qpath, qstring, query, resp) =>
fs.promises.open(path.join(clientDir, 'img', 'klipicon.ico'))
.then(fh => { resp.passImage('x-icon', resp.expectsBody() && fh.createReadStream()) })
.catch(err => { resp.httpError("Could not read favicon.ico: "+err) }))
.addService('apple-touch-icon.png', (qpath, qstring, query, resp) =>
fs.promises.open(path.join(clientDir, 'img', 'klipicon-180.png'))
.then(fh => { resp.passImage('png', resp.expectsBody() && fh.createReadStream()) })
.catch(err => { resp.httpError("Could not read klipicon-180.png: "+err) }))
.addService('manifest.webmanifest', (qpath, qstring, query, resp) => {
resp.passString('application/manifest+json',
'{"icons":['+
'{"src":"/img?file=klipicon-192.png","type":"image/png","sizes":"192x192"},'+
'{"src":"/img?file=klipicon-512.png","type":"image/png","sizes":"512x512"}]}');
})
.addService('favicon.svg', (qpath, qstring, query, resp) => { resp.passString('image/svg+xml', klipicon) })
.addService('player', (qpath, qstring, query, resp) => Promise.resolve().then(() => sources.httpCallable(query)).then(callable => {
const f = callable && callable[query.cmd];
if (typeof f === 'function') {
Promise.resolve().then(() => f(qstring, query)).then(ans => {
resp.passObj(ans === undefined ? { ok: true } : ans);
}).catch(err => {
log.debug(err);
resp.httpError(err.toString());
});
} else { resp.httpError("cmd not available: " + query.cmd) }
}).catch(err => {
resp.httpError("player not available: ("+err+")");
}))
.addService('audiostream', (qpath, qstring, query, resp) => Promise.resolve().then(() => sources.fetchStream(query, resp)).catch(err => {
resp.httpError("stream not available: "+err);
}))
.addService('stream_controls', (qpath, qstring, query, resp) => Promise.resolve().then(() => sources.pageLoad(query)).then(loadParam => {
resp.passProcessedTextFile('html', 'stream_controls.html', {
audiotype_list: JSON.stringify(opts.streamtypes),
...query,
...loadParam,
source_ix: query.source,
player_id: query.player_id || 0,
background_spec: opts.streambackground
}).catch(err => { resp.httpError("stream not available: "+err) });
}))
.addService('server_controls', (qpath, qstring, query, resp) => {
resp.passProcessedTextFile('html', 'server_controls.html', {
...query,
buttons: serverButtons,
source: JSON.stringify(sources[sources.lastChosenSourceIx()]),
player_id: "0",
background_spec: opts.serverbackground
});
})
.addService('recording', (qpath, qstring, query, resp) => Promise.resolve().then(() => {
const callable = sources.recordingIntf();
const f = callable && callable[query.cmd];
if (f) {
Promise.resolve().then(() => { f(query, resp) }).catch(err => {
log.debug(err);
resp.httpError(err.toString());
});
} else { resp.httpError("cmd not available: " + query.cmd) }
}))
.listen(opts.port);
// -----------------------------------------------------------------------------
// Set up remote and numpad
if (opts.remote || opts.numpad) { opts.eventsizes = JSON.parse(fs.readFileSync(path.join(opts.lib, 'eventsizes.json'))) }
if (opts.remote) { setupRemote(opts, sources.httpCallable ) }
if (opts.numpad && sources.radioChannels) {
const setupNumpadWhenAvailable = () => {
if (fs.existsSync(opts.numpad)) {
log.info("Setting up numpad key actions");
setupNumpad(opts, sources.httpCallable, i => { if (i < sources.radioChannels) { sources.radioPlay(i) } });
} else {
setTimeout(setupNumpadWhenAvailable, 10000); // retry every 10 seconds
}
}
setupNumpadWhenAvailable();
}
// -----------------------------------------------------------------------------
// Ready to go.
const ipAddr = () => {
const intfs = networkInterfaces();
for (const aa of Object.values(intfs)) {
for (const a of aa) {
if (!a.internal && ['4', 'ipv4'].includes(a.family.toString().toLowerCase())) {
return a.address;
}
}
}
return '127.0.0.1';
}
log.info("Klipspringer version "+JSON.parse(fs.readFileSync(path.join(opts.lib, 'node', 'package.json'))).version);
log.info("PID " + process.pid);
log.info("Server expects commands at http://" + ipAddr() + ":" + opts.port);
Version: v4.3.2.2 (2026-05-16T17:03:34+02:00)
Raw file
Source code overview
Klipspringer home