// 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