mpris-nowplaying 0.2.0

A websocket based MPRIS2 "now-playing" / status client.
<!DOCTYPE html>
<html lang="en-US">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />

    <script>
      const ADDRESS = "ws://127.0.0.1:32100";

      class WebsocketWrapper {
        ws = undefined;
        data = null;

        interval = undefined;
        intervalId = undefined;

        update;
        filter;

        connect() {
          const ws = new WebSocket(ADDRESS);

          ws.onclose = (e) => {
            console.log(
              "Socket is closed. Reconnect will be attempted in 5 seconds. Reason:",
              e.reason
            );

            this.ws = undefined;
            clearInterval(this.intervalId);
            this.intervalId = undefined;
            this.data = {};

            setTimeout(this.connect, 5000);
          };

          ws.onmessage = (e) => {
            if (!e.data) {
              this.data = null;
              return;
            }

            let data = null;

            if (typeof e.data == "object") {
              data = e.data;
            } else {
              try {
                data = JSON.parse(e.data);
              } catch {
                data = e.data;
              }
            }

            if (this.filter(data)) {
              this.data = data;
            }
          };

          this.ws = ws;
        }

        constructor(
          update = (ws, data) => {},
          interval = 1000,
          filter = (data) => true
        ) {
          this.update = update;
          this.filter = filter;
          this.interval = interval;

          this.intervalId = setInterval(() => {
            if (this.ws?.readyState === WebSocket.OPEN) {
              this.update(this.ws, this.data);
            }
          }, this.interval);

          this.connect();
        }
      }
    </script>

    <script>
      let imageFetcher = undefined;
      let imageUrl = undefined;
      let currentImage = undefined;

      function applyStatus(data, elements) {
        if (!imageFetcher) {
          imageFetcher = new WebsocketWrapper(
            (ws, data) => {
              ws.send("artwork/0");

              if (data != currentImage) {
                if (imageUrl) {
                  URL.revokeObjectURL(imageUrl);
                  imageUrl = undefined;
                }

                if (data) {
                  if (data instanceof Blob) {
                    imageUrl = URL.createObjectURL(data);
                  } else {
                    imageUrl = data;
                  }
                }

                currentImage = data;
              }
            },
            350,
            (data) => !!data
          );
        }

        const titleElement = elements["title"];
        const artistElement = elements["artist"];
        const albumElement = elements["album"];
        const statusElement = elements["status"];
        const barElement = elements["bar"];
        const barCircleElement = elements["barCircle"];
        const positionElement = elements["position"];
        const artElement = elements["art"];

        let paused = data?.playbackState == "paused";
        let position = data?.position;
        let metadata = data?.metadata;

        let title = metadata?.title;
        let artist = metadata?.artist;
        let album = metadata?.album;
        let length = metadata?.length;
        let art = metadata?.artwork?.at(0);

        if (titleElement) {
          if (title) {
            titleElement.textContent = title;
          } else {
            titleElement.textContent = "Nothing playing!";
          }
        }

        if (artistElement) {
          if (artist) {
            artistElement.textContent = `${artist}`;
          } else {
            artistElement.textContent = "";
          }
        }

        if (albumElement) {
          if (album) {
            albumElement.textContent = `${album}`;
          } else {
            albumElement.textContent = "";
          }
        }

        if (statusElement && barElement && barCircleElement) {
          if (position && length) {
            let posPercentage = (position / length) * 100.0;

            statusElement.hidden = false;
            barCircleElement.style.left = `${posPercentage}%`;
          } else {
            statusElement.hidden = true;
            barCircleElement.style.left = "0.0%";
          }
        }

        if (positionElement) {
          if (position && length) {
            let posSeconds = (Math.round(position / 1_000_000) % 60)
              .toString()
              .padStart(2, "0");
            let posMinutes = Math.floor(position / 1_000_000 / 60);
            let lenSeconds = (Math.round(length / 1_000_000) % 60)
              .toString()
              .padStart(2, "0");
            let lenMinutes = Math.floor(length / 1_000_000 / 60);

            var paused_text = "";
            if (paused) {
              paused_text = " (Paused!)";
            }

            positionElement.textContent =
              `${posMinutes}:${posSeconds} / ${lenMinutes}:${lenSeconds}` +
              paused_text;
          } else {
            positionElement.textContent = "";
          }
        }

        if (artElement) {
          if (art) {
            artElement.style.backgroundImage = `url("${imageUrl}")`;
          } else {
            artElement.style.backgroundImage = "";
          }
        }
      }
    </script>

    <script>
      function load() {
        const elements = {
          title: document.getElementById("np-title"),
          artist: document.getElementById("np-artist"),
          album: document.getElementById("np-album"),
          status: document.getElementById("np-status"),
          bar: document.getElementById("np-bar"),
          barCircle: document.getElementById("np-bar-circle"),
          position: document.getElementById("np-position"),
          art: document.getElementById("np-art"),
        };

        const ws = new WebsocketWrapper((ws, data) => {
          ws.send(null);
          applyStatus(data, elements);
        }, 250);
      }

      window.addEventListener("load", load);
    </script>

    <style>
      * {
        box-sizing: border-box;
      }

      :root {
        font-size: 24px;
        margin: 0;
        padding: 0;
      }

      body {
        margin: 0;
        padding: 0;
      }

      #nowplaying {
        width: 100vw;
        height: 100vh;

        display: flex;
      }

      #np-info {
        width: 50%;

        display: flex;
        flex-direction: column;

        padding: 8px;
        margin-right: 8px;
      }

      #np-text {
        text-align: center;
        height: 100%;
      }

      #np-bar {
        width: 100%;
        padding-top: 2px;
        margin-block: 6px;
        height: 10px;
        position: relative;
      }

      #np-bar-background {
        width: 100%;
        height: 100%;
        background-color: rgb(255, 255, 255);
        border: solid 3px rgb(70, 65, 83);
        border-radius: 8px;
        position: absolute;
      }

      #np-bar-circle {
        left: 0;
        top: 0;
        transform: translateX(-50%);
        width: 8px;
        height: 8px;
        background-color: rgb(248, 175, 175);
        border: solid 3px rgb(255, 134, 134);
        border-radius: 50%;
        box-sizing: content-box;
        position: absolute;
      }

      #np-art {
        width: 50%;
        opacity: 0.5;

        background-size: contain;
        background-repeat: no-repeat;
        background-position: center;
      }
    </style>
    <title>MPRIS Now-playing</title>
  </head>
  <body>
    <div id="nowplaying">
      <div id="np-info">
        <div id="np-text">
          <h3 id="np-title"></h3>
          <i id="np-album"></i>
          <p id="np-artist"></p>
        </div>
        <div id="np-status" hidden>
          <div id="np-bar">
            <div id="np-bar-background"></div>
            <div id="np-bar-circle"></div>
          </div>
          <div id="np-position"></div>
        </div>
      </div>
      <div id="np-art" />
    </div>
  </body>
</html>