kvasir-cli 0.1.2

Command-line client for the kvasir music library.
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Kvasir</title>
  <style>
    :root { color-scheme: light; }
    * { box-sizing: border-box; }
    body {
      margin: 0;
      font: 16px/1.45 "Segoe UI", system-ui, sans-serif;
      background: #f6f1e8;
      color: #2a241c;
    }
    header, main, footer { padding: 20px 24px; }
    header { display: flex; justify-content: space-between; gap: 16px; align-items: center; }
    h1 { margin: 0; font-size: 28px; font-weight: 650; letter-spacing: -0.03em; }
    p.lede { margin: 4px 0 0; color: #6d6256; }
    button, select, input {
      font: inherit;
      border-radius: 10px;
      border: 1px solid #d9cfc2;
      background: white;
      color: inherit;
    }
    button { padding: 10px 14px; cursor: pointer; }
    button.primary { background: #2a241c; color: #f6f1e8; border-color: #2a241c; }
    button:disabled { opacity: 0.45; cursor: default; }
    .row { display: flex; gap: 8px; }
    input[type="text"] { flex: 1; padding: 12px 14px; }
    #results { display: flex; flex-direction: column; gap: 8px; }
    article {
      display: flex; justify-content: space-between; gap: 12px; align-items: center;
      background: white; border-radius: 12px; padding: 12px 14px;
    }
    article strong { display: block; }
    article span { color: #6d6256; font-size: 14px; }
    footer { display: grid; gap: 10px; }
    label { display: flex; gap: 8px; align-items: center; }
    #folder { flex: 1; }
    #status { min-height: 1.4em; color: #6d6256; }
    #status.ok { color: #1f7a45; }
    #status.err { color: #9d2c2c; }
    .hidden { display: none; }
  </style>
</head>
<body>
  <header>
    <div>
      <h1>Kvasir</h1>
      <p class="lede" id="account">Not signed in</p>
    </div>
    <div class="row">
      <button id="signin" class="primary" type="button">Sign in with Deezer</button>
      <button id="signout" class="hidden" type="button">Sign out</button>
    </div>
  </header>
  <main>
    <form id="go" class="row">
      <input id="query" type="text" placeholder="Search, or paste a Deezer, Spotify, Tidal, or YouTube link" autocomplete="off">
      <button class="primary" type="submit">Go</button>
    </form>
    <div id="results" style="margin-top: 16px"></div>
  </main>
  <footer>
    <label>Quality
      <select id="quality">
        <option value="128">128 kbps</option>
        <option value="320">320 kbps</option>
        <option value="flac">FLAC</option>
      </select>
    </label>
    <div class="row">
      <input id="folder" type="text" spellcheck="false">
      <button id="browse" type="button">Choose folder</button>
    </div>
    <div id="status"></div>
    <p class="lede" id="configdir"></p>
    <div>
      <button id="reset" type="button">Delete saved login and start over</button>
    </div>
  </footer>
  <script>
    const send = (msg) => window.ipc.postMessage(JSON.stringify(msg));
    const $ = (id) => document.getElementById(id);
    function setBusy(on) {
      document.body.classList.toggle("busy", on);
      for (const el of document.querySelectorAll("button, input, select")) el.disabled = on && el.id !== "reset";
    }
    function setStatus(level, text) {
      const node = $("status");
      node.className = level || "";
      node.textContent = text || "";
    }
    window.kvasir = window.kvasir || {};
    window.kvasir.send = send;
    window.kvasir.recv = (msg) => {
      if (msg.op === "state") {
        $("account").textContent = msg.signed_in ? ("Signed in" + (msg.name ? " as " + msg.name : "")) : "Not signed in";
        $("signin").classList.toggle("hidden", msg.signed_in);
        $("signout").classList.toggle("hidden", !msg.signed_in);
        $("quality").value = ["128", "320", "flac"].includes(msg.quality) ? msg.quality : "flac";
        $("folder").value = msg.download_dir || "";
        $("configdir").textContent = "Settings stay in " + msg.config_dir + ". Close the window and run kvasir reset to delete that folder.";
      } else if (msg.op === "results") {
        const box = $("results");
        box.replaceChildren();
        if (!msg.tracks || !msg.tracks.length) {
          setStatus("err", "Nothing matched.");
          return;
        }
        setStatus("ok", msg.tracks.length + " track(s). Download one, or paste a link into the box.");
        for (const track of msg.tracks) {
          const row = document.createElement("article");
          const text = document.createElement("div");
          const title = document.createElement("strong");
          title.textContent = track.title + " — " + track.artist;
          const note = document.createElement("span");
          note.textContent = track.album + " · " + track.duration;
          text.append(title, note);
          const button = document.createElement("button");
          button.type = "button";
          button.textContent = "Download";
          button.addEventListener("click", () => send({ op: "download", id: track.id }));
          row.append(text, button);
          box.append(row);
        }
      } else if (msg.op === "downloaded") {
        const paths = msg.paths || [];
        setStatus("ok", paths.length ? ("Saved " + paths[paths.length - 1]) : "Nothing was written.");
      } else if (msg.op === "status") {
        setStatus(msg.level, msg.text);
      } else if (msg.op === "busy") {
        setBusy(!!msg.on);
        if (msg.on) setStatus("info", "Working…");
      }
    };
    $("signin").addEventListener("click", () => send({ op: "login" }));
    $("signout").addEventListener("click", () => send({ op: "logout" }));
    $("reset").addEventListener("click", () => send({ op: "reset" }));
    $("browse").addEventListener("click", () => send({ op: "browse" }));
    $("quality").addEventListener("change", () => send({ op: "settings", quality: $("quality").value, download_dir: $("folder").value }));
    $("folder").addEventListener("change", () => send({ op: "settings", quality: $("quality").value, download_dir: $("folder").value }));
    $("go").addEventListener("submit", (event) => {
      event.preventDefault();
      send({ op: "go", query: $("query").value });
    });
    send({ op: "ready" });
  </script>
</body>
</html>