From a0ae1b422d8b704fb4119c2c8486adcc49ea6bf2 Mon Sep 17 00:00:00 2001
From: Jeeves <guydoodlesdev@gmail.com>
Date: Tue, 18 Feb 2025 22:03:27 -0700
Subject: [PATCH] init

---
 deno.json           |   9 ++++
 deno.lock           |  97 ++++++++++++++++++++++++++++++++++++++++
 flake.lock          |  61 +++++++++++++++++++++++++
 flake.nix           |  15 +++++++
 shell.nix           |   5 +++
 src/main.ts         | 106 ++++++++++++++++++++++++++++++++++++++++++++
 web/statusline.html |  12 +++++
 web/statusline.js   |  46 +++++++++++++++++++
 web/style.css       |  20 +++++++++
 9 files changed, 371 insertions(+)
 create mode 100644 deno.json
 create mode 100644 deno.lock
 create mode 100644 flake.lock
 create mode 100644 flake.nix
 create mode 100644 shell.nix
 create mode 100644 src/main.ts
 create mode 100644 web/statusline.html
 create mode 100644 web/statusline.js
 create mode 100644 web/style.css

diff --git a/deno.json b/deno.json
new file mode 100644
index 0000000..565768e
--- /dev/null
+++ b/deno.json
@@ -0,0 +1,9 @@
+{
+  "tasks": {
+    "dev": "deno run --allow-net --allow-read --watch src/main.ts"
+  },
+  "imports": {
+    "@oak/oak": "jsr:@oak/oak@^17.1.4",
+    "@std/assert": "jsr:@std/assert@1"
+  }
+}
diff --git a/deno.lock b/deno.lock
new file mode 100644
index 0000000..3fa19fc
--- /dev/null
+++ b/deno.lock
@@ -0,0 +1,97 @@
+{
+  "version": "4",
+  "specifiers": {
+    "jsr:@oak/commons@1": "1.0.0",
+    "jsr:@oak/oak@^17.1.4": "17.1.4",
+    "jsr:@std/assert@1": "1.0.11",
+    "jsr:@std/bytes@1": "1.0.5",
+    "jsr:@std/crypto@1": "1.0.4",
+    "jsr:@std/encoding@1": "1.0.7",
+    "jsr:@std/encoding@^1.0.7": "1.0.7",
+    "jsr:@std/fmt@0.223": "0.223.0",
+    "jsr:@std/http@1": "1.0.13",
+    "jsr:@std/internal@^1.0.5": "1.0.5",
+    "jsr:@std/media-types@1": "1.1.0",
+    "jsr:@std/path@1": "1.0.8",
+    "npm:fast-xml-parser@*": "4.5.2",
+    "npm:path-to-regexp@^6.3.0": "6.3.0"
+  },
+  "jsr": {
+    "@oak/commons@1.0.0": {
+      "integrity": "49805b55603c3627a9d6235c0655aa2b6222d3036b3a13ff0380c16368f607ac",
+      "dependencies": [
+        "jsr:@std/assert",
+        "jsr:@std/bytes",
+        "jsr:@std/crypto",
+        "jsr:@std/encoding@1",
+        "jsr:@std/http",
+        "jsr:@std/media-types"
+      ]
+    },
+    "@oak/oak@17.1.4": {
+      "integrity": "60530b582bf276ff741e39cc664026781aa08dd5f2bc5134d756cc427bf2c13e",
+      "dependencies": [
+        "jsr:@oak/commons",
+        "jsr:@std/assert",
+        "jsr:@std/bytes",
+        "jsr:@std/http",
+        "jsr:@std/media-types",
+        "jsr:@std/path",
+        "npm:path-to-regexp"
+      ]
+    },
+    "@std/assert@1.0.11": {
+      "integrity": "2461ef3c368fe88bc60e186e7744a93112f16fd110022e113a0849e94d1c83c1",
+      "dependencies": [
+        "jsr:@std/internal"
+      ]
+    },
+    "@std/bytes@1.0.5": {
+      "integrity": "4465dd739d7963d964c809202ebea6d5c6b8e3829ef25c6a224290fbb8a1021e"
+    },
+    "@std/crypto@1.0.4": {
+      "integrity": "cee245c453bd5366207f4d8aa25ea3e9c86cecad2be3fefcaa6cb17203d79340"
+    },
+    "@std/encoding@1.0.7": {
+      "integrity": "f631247c1698fef289f2de9e2a33d571e46133b38d042905e3eac3715030a82d"
+    },
+    "@std/fmt@0.223.0": {
+      "integrity": "6deb37794127dfc7d7bded2586b9fc6f5d50e62a8134846608baf71ffc1a5208"
+    },
+    "@std/http@1.0.13": {
+      "integrity": "d29618b982f7ae44380111f7e5b43da59b15db64101198bb5f77100d44eb1e1e",
+      "dependencies": [
+        "jsr:@std/encoding@^1.0.7"
+      ]
+    },
+    "@std/internal@1.0.5": {
+      "integrity": "54a546004f769c1ac9e025abd15a76b6671ddc9687e2313b67376125650dc7ba"
+    },
+    "@std/media-types@1.1.0": {
+      "integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4"
+    },
+    "@std/path@1.0.8": {
+      "integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be"
+    }
+  },
+  "npm": {
+    "fast-xml-parser@4.5.2": {
+      "integrity": "sha512-xmnYV9o0StIz/0ArdzmWTxn9oDy0lH8Z80/8X/TD2EUQKXY4DHxoT9mYBqgGIG17DgddCJtH1M6DriMbalNsAA==",
+      "dependencies": [
+        "strnum"
+      ]
+    },
+    "path-to-regexp@6.3.0": {
+      "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="
+    },
+    "strnum@1.0.5": {
+      "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA=="
+    }
+  },
+  "workspace": {
+    "dependencies": [
+      "jsr:@oak/oak@^17.1.4",
+      "jsr:@std/assert@1"
+    ]
+  }
+}
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 0000000..9cc4e2e
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,61 @@
+{
+  "nodes": {
+    "flake-utils": {
+      "inputs": {
+        "systems": "systems"
+      },
+      "locked": {
+        "lastModified": 1731533236,
+        "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
+        "type": "github"
+      },
+      "original": {
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "type": "github"
+      }
+    },
+    "nixpkgs": {
+      "locked": {
+        "lastModified": 1739736696,
+        "narHash": "sha256-zON2GNBkzsIyALlOCFiEBcIjI4w38GYOb+P+R4S8Jsw=",
+        "owner": "nixos",
+        "repo": "nixpkgs",
+        "rev": "d74a2335ac9c133d6bbec9fc98d91a77f1604c1f",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nixos",
+        "ref": "nixos-unstable",
+        "repo": "nixpkgs",
+        "type": "github"
+      }
+    },
+    "root": {
+      "inputs": {
+        "flake-utils": "flake-utils",
+        "nixpkgs": "nixpkgs"
+      }
+    },
+    "systems": {
+      "locked": {
+        "lastModified": 1681028828,
+        "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+        "owner": "nix-systems",
+        "repo": "default",
+        "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nix-systems",
+        "repo": "default",
+        "type": "github"
+      }
+    }
+  },
+  "root": "root",
+  "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000..82932f8
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,15 @@
+{
+  inputs = {
+    nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
+    flake-utils.url = "github:numtide/flake-utils";
+  };
+
+  outputs = { self, nixpkgs, flake-utils }:
+    flake-utils.lib.eachDefaultSystem(system: let
+      pkgs = nixpkgs.legacyPackages.${system};
+    in {
+      # packages.default = pkgs.callPackage ./default.nix {};
+      devShells.default = import ./shell.nix { inherit pkgs; };
+    });
+}
+
diff --git a/shell.nix b/shell.nix
new file mode 100644
index 0000000..217f314
--- /dev/null
+++ b/shell.nix
@@ -0,0 +1,5 @@
+{ pkgs ? import <nixpkgs> {} }:
+pkgs.mkShell {
+  packages = with pkgs; [deno];
+}
+
diff --git a/src/main.ts b/src/main.ts
new file mode 100644
index 0000000..87223c8
--- /dev/null
+++ b/src/main.ts
@@ -0,0 +1,106 @@
+import { Application, Router } from "@oak/oak";
+import { XMLParser } from "npm:fast-xml-parser";
+
+const router = new Router();
+
+let wsClients: WebSocket[] = [];
+
+router.get("/", (context) => {
+  if (context.isUpgradable) {
+    const ws = context.upgrade();
+    ws.onopen = () => wsClients.push(ws);
+    ws.onclose = () => wsClients = wsClients.filter((client) => client != ws);
+  }
+});
+
+router
+  .get("/statusline", async (ctx) => {
+    ctx.response.body = await Deno.readFile("web/statusline.html");
+  })
+  .get("/statusline.js", async (ctx) => {
+    ctx.response.body = await Deno.readFile("web/statusline.js");
+  })
+  .get("/style.css", async (ctx) => {
+    ctx.response.body = await Deno.readFile("web/style.css");
+  });
+
+const app = new Application();
+app.use(router.routes());
+app.use(router.allowedMethods());
+
+app.addEventListener("listen", ({ hostname, port }) => {
+  console.log(`Start listening on ${hostname}:${port}`);
+});
+
+app.listen({ port: 8012 });
+
+const topic =
+  "Something Fun For Everyone With Streamboy!!!!!!!! git.jeevio.xyz/jeeves/streamboy";
+
+setInterval(async () => {
+  const songinfo = await getVlcSongInfo();
+  const data = {
+    blocks: [
+      { text: topic, color: "#ffffff" },
+      { text: `♪ ${songinfo.title} - ${songinfo.artist}`, color: "#ffffff" },
+    ],
+  };
+  for (const ws of wsClients) {
+    // ws.send(
+    //   `${topic}    |    ♪ ${songinfo.title} - ${songinfo.artist}    |    `,
+    // );
+    //
+    ws.send(JSON.stringify(data));
+  }
+}, 900);
+
+interface SongInfo {
+  title?: string;
+  artist?: string;
+  album?: string;
+}
+
+async function getVlcSongInfo(): Promise<SongInfo> {
+  const parser = new XMLParser({ ignoreAttributes: false });
+  const password = "1234";
+
+  const res = await fetch("http://localhost:8080/requests/status.xml", {
+    headers: { authorization: `Basic ${btoa(`:${password}`)}` },
+  });
+
+  const json = parser.parse(await res.text());
+
+  const songinfo: SongInfo = {};
+
+  for (const category of json.root.information.category) {
+    if (category["@_name"] != "meta") continue;
+    for (const property of category.info) {
+      if (property["@_name"] == "title") {
+        songinfo.title = processBadXmlString(property["#text"]);
+      } else if (property["@_name"] == "artist") {
+        songinfo.artist = processBadXmlString(property["#text"]);
+      } else if (property["@_name"] == "album") {
+        songinfo.album = processBadXmlString(property["#text"]);
+      }
+    }
+  }
+
+  return songinfo;
+}
+
+function processBadXmlString(str: string): string {
+  let newStr = str;
+
+  while (true) {
+    const amp = newStr.indexOf("&#");
+    if (amp > 0) {
+      const semi = newStr.indexOf(";", amp);
+      if (semi > 0) {
+        const int = String.fromCharCode(parseInt(newStr.slice(amp + 2, semi)));
+        newStr = newStr.replace(newStr.slice(amp, semi + 1), int);
+      } else break;
+    } else break;
+  }
+
+  return newStr;
+}
diff --git a/web/statusline.html b/web/statusline.html
new file mode 100644
index 0000000..ec822d8
--- /dev/null
+++ b/web/statusline.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+
+<head>
+  <link rel="stylesheet" href="style.css" />
+</head>
+
+<body>
+  <p id="status-line">Streamboy is connecting...</p>
+
+  <script src="statusline.js"></script>
+</body>
+
diff --git a/web/statusline.js b/web/statusline.js
new file mode 100644
index 0000000..a6797dd
--- /dev/null
+++ b/web/statusline.js
@@ -0,0 +1,46 @@
+const statusLineElement = document.getElementById("status-line");
+const statusLineElement2 = statusLineElement.cloneNode();
+statusLineElement2.id = "status-line2";
+document.body.appendChild(statusLineElement2);
+
+let status = "Streamboy is connecting...";
+let scroll = 0;
+
+setInterval(() => {
+  if (scroll >= status.length) scroll = 0;
+  // if (scroll == 0) {
+  //   statusLineElement.textContent = status;
+  // } else {
+  //   let string = "";
+  //   string += status.slice(scroll);
+  //   string += status.slice(0, scroll);
+  //   statusLineElement.textContent = string;
+  // }
+  const stringWidth = 8 * status.length;
+  statusLineElement.style.left = `${-8 * scroll}px`;
+  statusLineElement2.style.left = `${-8 * scroll + stringWidth}px`;
+  // console.log(statusLineElement.style.left)
+  statusLineElement.textContent = status;
+  statusLineElement2.textContent = status;
+  scroll += 1;
+}, 750);
+
+const socket = new WebSocket("ws://localhost:8012");
+
+socket.addEventListener("message", (event) => {
+  const data = JSON.parse(event.data);
+
+  let string = "";
+  for (const block of data.blocks) {
+    string += block.text;
+    string += "    |    ";
+  }
+  status = string;
+});
+
+// const status = {
+//   blocks: [
+//     "Something Fun For Everyone With Streamboy!!!!!!!! git.jeevio.xyz/jeeves/streamboy",
+//     "♪ Bonhomme de Neige (EarthBound) - Ridley Snipes feat. Earth Kid",
+//   ],
+// };
diff --git a/web/style.css b/web/style.css
new file mode 100644
index 0000000..2e66959
--- /dev/null
+++ b/web/style.css
@@ -0,0 +1,20 @@
+body {
+  font-family: Unifont;
+  background-color: black;
+  color: white;
+  margin: 0px auto;
+  overflow: hidden;
+}
+
+#gone {
+  font-size: 64px;
+}
+
+#status-line, #status-line2 {
+  font-size: 16px;
+  margin: 0px;
+  white-space: preserve nowrap;
+  max-width: none;
+  text-align: center;
+  position: absolute;
+}