ota-tv-web/src/zap.ts
2025-04-01 19:09:11 -07:00

164 lines
4.7 KiB
TypeScript

import { spawn, ChildProcessWithoutNullStreams } from "child_process";
import * as fs from "fs";
export interface IZap {
process: ChildProcessWithoutNullStreams | null,
channel: string,
adapter: 0 | 1
strength: {
signal: string;
cn: string;
},
}
export default class Zap {
private zap0: IZap;
private zap1: IZap;
private channelNameList: string[];
private fileName: string;
private regex = /Signal=\s*(-?\d+(\.\d+)?dBm)\s+C\/N=\s*(\d+(\.\d+)?dB)/;
public constructor(fileName = "dvb_channel.conf", channel = "ION") {
const zap0: IZap = {
process: null,
channel,
adapter: 0,
strength: {
signal: "None",
cn: "None"
}
}
const zap1: IZap = {
process: null,
channel,
adapter: 1,
strength: {
signal: "None",
cn: "None"
}
}
this.zap0 = zap0;
this.zap1 = zap1;
const file = fs.readFileSync('./' + fileName, "utf-8");
const lines = file.split("\n").map(line => line.trim()).filter(line => line && !line.startsWith("#")).map(line => line.split(":")[0]);
this.channelNameList = lines;
this.fileName = fileName;
}
public getChannels(): string[] {
return this.channelNameList;
}
public getSignal(adapter: number) {
if(adapter == 0){
return this.zap0.strength;
}
else if (adapter == 1){
return this.zap1.strength;
}
else {
return {signal: 'N/A', cn: 'N/A'}
}
}
private nextChannel(channel: string) :string {
const size = this.channelNameList.length;
const currentIndex = this.channelNameList.indexOf(channel);
if (currentIndex >= 0) {
return this.channelNameList[this.mod(currentIndex + 1, size)];
}
else {
return channel;
}
}
private mod(n: number, m: number) :number{
return ((n % m) + m) % m;
}
private previousChannel(channel: string):string {
const size = this.channelNameList.length;
const currentIndex = this.channelNameList.indexOf(channel);
if (currentIndex >= 0) {
return this.channelNameList[this.mod(currentIndex - 1, size)];
}
else {
return channel;
}
}
private cleanup(proc: ChildProcessWithoutNullStreams): Promise<void> {
proc.kill('SIGHUP');
return new Promise((resolve, reject) => {
proc.once('exit', () =>
resolve()
)
proc.on("error", (err: Error) =>
reject(err)
);
})
}
public async zapTo(channel: string, adapter: 0 | 1 = 0): Promise<IZap> {
let zap = adapter == 0 ? this.zap0 : this.zap1;
let verifiedChannel: string;
if (channel == '+') {
verifiedChannel = this.nextChannel(zap.channel);
}
else if (channel == '-') {
verifiedChannel = this.previousChannel(zap.channel);
}
else {
if (!this.channelNameList.includes(channel)) {
return Promise.reject(new Error("Invalid Channel name"));
}
else {
verifiedChannel = channel;
}
}
const cmd = 'dvbv5-zap'
const args: string[] = ["-r", "-C", "US", "-I", "ZAP", "-c", this.fileName, "-a", adapter.toString(), verifiedChannel];
if (zap.process != null) {
await this.cleanup(zap.process).catch(err => Promise.reject(err));
}
zap.process = spawn(cmd, args);
return new Promise((resolve, reject) => {
let lockTimer: NodeJS.Timeout;
zap.process.stderr.on("data", (data) => {
const output = data.toString();
if (/Lock/.test(output)) {
clearTimeout(lockTimer);
const match = output.match(this.regex);
zap.channel = verifiedChannel;
zap.strength = {
signal: match[1],
cn: match[3]
}
resolve(zap);
}
if (/Not locked/.test(output)) {
clearTimeout(lockTimer);
zap.process.kill("SIGHUP");
}
});
zap.process.on("exit", (code) => {
reject(new Error("Unexpected exit of Zap"));
});
lockTimer = setTimeout(() => {
reject(new Error("Failed to Zap in time"));
}, 5000);
})
}
}