From 358f9c8784d0c4e729dec7d2c7af3dc0e18be472 Mon Sep 17 00:00:00 2001 From: david Date: Tue, 1 Apr 2025 16:29:56 -0700 Subject: [PATCH] API support for channel change --- dvb_channel.conf | 6 +++ package.json | 10 ++-- src/http.ts | 47 ++++++++++++++---- src/server.ts | 44 ++++++++++++++-- src/zap.ts | 127 +++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 217 insertions(+), 17 deletions(-) create mode 100644 dvb_channel.conf create mode 100644 src/zap.ts diff --git a/dvb_channel.conf b/dvb_channel.conf new file mode 100644 index 0000000..2617fe6 --- /dev/null +++ b/dvb_channel.conf @@ -0,0 +1,6 @@ +ION:521028615:8VSB:49:52:3 +KATU:533028615:8VSB:49:52:3 +KOIN-HD:539028615:8VSB:49:52:3 +KGW:545028615:8VSB:49:52:3 +KBLN-DT:575028615:8VSB:49:52:1 +TBN HD:581028615:8VSB:49:52:3 diff --git a/package.json b/package.json index a1d92f0..fa1d49d 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,13 @@ "ws": "^8.18.0" }, "scripts": { - "build": "npx tsc --skipLibCheck --outDir dist src/server.ts", + "build:scss": "npx sass src/static/css:dist/static/css", + "copy:html": "cp src/static/index.html dist/static", + "build:js:fe": "npx tsc src/static/js/*.ts --outDir dist/static/js", + "build:js:be": "npx tsc --skipLibCheck src/*.ts --outDir dist", + "build": "npm run build:js:fe && npm run build:js:be && npm run build:scss && npm run copy:html", "start": "npm run build && node dist/server.js", - "debug": "npx ts-node src/server.ts" + "dev": "npm run build && npx ts-node src/server.ts" }, "devDependencies": { "@types/node": "^22.10.2", @@ -15,4 +19,4 @@ "ts-node": "^10.9.2", "typescript": "^5.8.2" } -} +} \ No newline at end of file diff --git a/src/http.ts b/src/http.ts index 6fa3235..882ba0a 100644 --- a/src/http.ts +++ b/src/http.ts @@ -8,20 +8,45 @@ export default class HttpServer { private httpServer: http.Server; private port: number; private root: string; - public constructor(port: number, root : string) { + public constructor(port: number, root: string, tune: (ch: string, adp?: number) => void) { this.port = port; this.root = root; this.httpServer = http.createServer((req, res) => { - const filePath = path.join(root, req.url === "/" ? "index.html" : req.url || ""); - fs.readFile(filePath, (err, data) => { - if (err) { - res.writeHead(404); - res.end("Not Found"); - } else { - res.writeHead(200); - res.end(data); + let status: number = 404; + let body: any; + const url = new URL(req.url, `http://${req.headers.host}`); + const pathname = path.normalize(url.pathname); + if (req.method === 'GET') { + const filePath = path.join(root, path.extname(pathname) === '' ? pathname + "/index.html" : pathname); + try { + body = fs.readFileSync(filePath); + status = 200; } - }); + catch (err) { + body = "Invalid File" + } + + } + else if (req.method === 'PUT' && pathname.startsWith('/api/')) { + const query = pathname.split('/'); + const api = query[2]; + switch (api) { + case "channel": + const channel = query[3]; + const adapter = parseInt(url.searchParams.get('adapter')); + tune(channel, adapter); + break; + default: + body = "Invalid API Endpoint" + break; + } + } + else { + body = "Invalid Request" + } + res.writeHead(status); + res.end(body); + }); } public start() { @@ -34,5 +59,7 @@ export default class HttpServer { + + } diff --git a/src/server.ts b/src/server.ts index 72cad74..0582ac4 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,15 +1,51 @@ import HttpServer from './http'; import TVWebSocket from './ws'; +import Zap, { IZap } from './zap'; -const HTTP_PORT = process.env.HTTP_PORT ? parseInt(process.env.HTTP_PORT,10) : 8080; +import * as readline from 'readline'; + +const HTTP_PORT = process.env.HTTP_PORT ? parseInt(process.env.HTTP_PORT, 10) : 8080; const WS_PORT = process.env.WS_PORT ? parseInt(process.env.WS_PORT, 10) : 3001; -const STATIC_ROOT = process.cwd() + "/dist/static";; +const STATIC_ROOT = process.cwd() + "/dist/static"; +const TV_DEV_0 = process.env.TV_DEV_0 ?? '/dev/dvb/adapter0/dvr0' +const TV_DEV_1 = process.env.TV_DEV_1 ? '/dev/dvb/adapter0/dvr1' : null; -const httpServer = new HttpServer(HTTP_PORT, STATIC_ROOT); +const zap = new Zap(); + + +const tune = (reqChannel: string, reqAdapter?: number) => { + const adapter = reqAdapter === 0 || reqAdapter === 1 ? reqAdapter : 0; + zap.zapTo(reqChannel, adapter).then((zap: IZap) => { + console.log(`Tuned ${zap.adapter} to ${zap.channel}`) + }).catch((err: Error) => { + console.error(err.message); + }); +} + +const httpServer = new HttpServer(HTTP_PORT, STATIC_ROOT, tune); const tvWebSocket = new TVWebSocket(WS_PORT); - httpServer.start(); +process.stdin.setEncoding("utf8"); +process.stdin.resume(); + +console.log("Enter Channel Name:"); + +process.stdin.on("data", async (data: string) => { + const input = data.trim(); + console.log(`Received: "${input}"`); + await zap.zapTo(input).then((zap: IZap) => { + console.log(`Tuned ${zap.adapter} to ${zap.channel}`) + + }).catch((err: Error) => { + console.error(err.message); + }); + +}); + + + + diff --git a/src/zap.ts b/src/zap.ts new file mode 100644 index 0000000..d912b73 --- /dev/null +++ b/src/zap.ts @@ -0,0 +1,127 @@ +import { spawn, ChildProcessWithoutNullStreams } from "child_process"; +import * as fs from "fs"; + +export interface IZap { + process: ChildProcessWithoutNullStreams | null, + channel: string, + adapter: 0 | 1 +} + +export default class Zap { + private zap0: IZap; + private zap1: IZap; + private channelNameList: string[]; + private fileName: string; + public constructor(fileName = "dvb_channel.conf", channel = "ION") { + const zap0: IZap = { + process: null, + channel, + adapter: 0 + } + const zap1: IZap = { + process: null, + channel, + adapter: 1 + } + 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; + + } + + private nextChannel(channel: 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) { + return ((n % m) + m) % m; + } + + private previousChannel(channel: 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 { + 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 { + 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); + zap.channel = verifiedChannel; + 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); + }) + + } +} \ No newline at end of file