// 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 { createPlayerCommon } from './player_common.js';
import { log } from './log.js';

export const createVlcPlayer = (name, vlc, resource, extraArgs) => {
    const args = ['-I', 'rc'].concat(extraArgs || [], resource);

    let status = { };
    let trackTime = 0, trackTimeTime = 0;
    let errMsg = [];

    const intf = createPlayerCommon(name, vlc, args);

    intf.readyPattern = "Command Line Interface initialized";
    intf.player_start_time = Date.now();

    const outputStatus = (includeTracks) => intf.writeProcStdin('info\n')
          .then(() => intf.writeProcStdin('status\n'))
          .then(() => intf.writeProcStdin('get_time\n'))
          .then(() => includeTracks ? intf.writeProcStdin('playlist\n') : null)
          .catch(() => { });

    intf.togglePauseTo = (target) => {
        return intf.writeProcStdin("pause\n");
    }

    intf.processStdout = (bufferedString) => {
        let off = 0;
        const infoM = bufferedString.match(/\[ end of stream info \]/mi);
        if (infoM) {
            for (const line of bufferedString.slice(0, infoM.index).split(/[\n\r]+/)) {
                const m = line.match(/^[^a-z_]*([^:]+?)\s*:\s*(.*?)\s*$/i);
                if (m) {
                    const label = m[1].toLowerCase().replaceAll(/ /g, '_');
                    const value = ['track_number', 'bits_per_sample'].includes(label) ? +m[2] : m[2];
                    if (value !== '') { status[label] = value }
                }
            }
            off += infoM.index + infoM[0].length;
        }
        const pauseM = bufferedString.slice(off).match(/\(\s*state\s*(playing)|(paused)\s*\)\s*/mi);
        if (pauseM) {
            status.paused = !!pauseM[2];
            off += pauseM.index + pauseM[0].length;
        } else if (!status.hasOwnProperty('paused')) {
            status.paused = true; // stop the clock by default
        }
        const timeM = bufferedString.slice(off).match(/([0-9]+)$/m);
        if (timeM && timeM[1] > 0 && status.sample_rate) {
            trackTime = +timeM[1];
            trackTimeTime = Date.now();
            off += timeM.index + timeM[0].length;
        }
        const playlistM = bufferedString.match(/\[ end of playlist \]/mi);
        if (playlistM) {
            let tracks = [];
            for (const line of bufferedString.slice(0, playlistM.index).split(/[\n\r]+/)) {
                const m = line.match(/^[^0-9]*([0-9]+)\s+-\s+(.*)\s+\([0-9:]+\)(?:\s+\[.*\])?$/i);
                if (m) {
                    if (tracks.length === 0) { intf.playlistOff = +m[1] }
                    tracks.push(m[2].replace(/_/g, ' '));
                }
            }
            status.tracks = tracks;
            off += playlistM.index + playlistM[0].length;
        }
        return off;
    }

    intf.noteStderr = (buf) => {
        errMsg = errMsg.filter(e => Date.now() - e[1] < 20000);
        for (const m of buf.toString().matchAll(/^.*error.*$/gmi)) {
            errMsg.push([m[0], Date.now()]);
        }
        // something happened, renew status
        outputStatus().catch(err => { log.error(name+" info command failed: "+err) });
    }

    // It's hopeless to sync everything perfectly (the time in particular) with
    // the rc interface. So just do something simple: let get_status take a
    // second or so, and it will update the client reasonably quickly.

    intf.httpCallable.get_status = (qstring, query) => new Promise((resolve, reject) => {
        setTimeout(() => {
            const updateTracks = query && query.info_id !== undefined && (query.info_id !== intf.id || Date.now() - intf.player_start_time < 10000);
            status = { new_info: updateTracks };
            outputStatus(updateTracks).then(() => {
                setTimeout(() => {
                    if (errMsg.length > 0) {
                        status.error = errMsg.reduce((acc, e) => { return acc + e[0] + "<br>"}, "");
                    }
                    if (trackTimeTime) {
                        const since =  (Date.now() - trackTimeTime)/1000;
                        status.track_time = trackTime + (status.paused ? 0 : since);
                    }
                    if (query && query.info_id) { status.info_id = intf.id }
                    resolve(status);
                }, 500);
            }).catch(err => { reject(err) });
        }, 1500);
    });

    intf.httpCallable.next = () => {
        intf.writeProcStdin('next\n').catch(() => { });
    }

    intf.httpCallable.prev = (qstring, query) => {
        // prev restarts current track, unless repeated within 1s
        intf.writeProcStdin(!(query.rept < 1000) ? 'seek 0\n' : 'prev\n').catch(() => { });
    }

    intf.httpCallable.seekr = (qstring, query) => {
        const t = query.time || 0;
        intf.writeProcStdin((t < 0 ? 'seek ' : 'seek +')+t+'\n').catch(() => { });
    }

    intf.httpCallable.seeka = (qstring, query) => {
        const tr = +(query.track || 1) + (intf.playlistOff || 0);
        intf.writeProcStdin('goto +'+tr+'\n').catch(() => { });
        if (query.time !== undefined) {
            const ti = query.time;
            intf.writeProcStdin((ti < 0 ? 'seek ' : 'seek +')+ti+'\n').catch(() => { });
        }
    }

    return intf;
}

Version: v4.3.2.2 (2026-05-16T17:03:34+02:00)
Raw file
Source code overview
Klipspringer home