commit e394d45dba7c024d0a5057945d4261fe623c8048
parent db67d60f9d5278957dde85a2cb078dc7c573eb36
Author: mtmn <miro@haravara.org>
Date: Sat, 11 Apr 2026 02:24:24 +0200
feat: fix builds, inline htmx to ps-halogen
Former-commit-id: fb42be49616b3310c9207a406763988b992eb8da
Diffstat:
14 files changed, 2713 insertions(+), 505 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -1,3 +1,5 @@
+*.js
output/
.spago/
node_modules/
+.env
diff --git a/.purs-repl b/.purs-repl
@@ -0,0 +1 @@
+import Prelude
diff --git a/index.html b/index.html
@@ -1,250 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>scrobbler.mtmn.name</title>
- <script src="https://unpkg.com/htmx.org@2.0.7"></script>
- <style>
- body {
- font-family: 'Courier New', 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
- background: #332d38;
- color: #ffffff;
- margin: 0;
- padding: 20px;
- line-height: 1.6;
- }
-
- .container {
- max-width: 800px;
- margin: 0 auto;
- }
-
- h1 {
- color: #ffffff;
- margin-bottom: 20px;
- font-size: 24px;
- }
-
- ul {
- list-style: none;
- padding: 0;
- margin: 0 0 20px 0;
- }
-
- li {
- background: rgba(0, 0, 0, 0.3);
- border: 1px solid rgba(80, 68, 127, 0.5);
- border-radius: 4px;
- padding: 15px;
- margin-bottom: 10px;
- display: flex;
- justify-content: space-between;
- align-items: center;
- }
-
- li.success {
- background: rgba(185, 208, 170, 0.2);
- border-color: rgba(185, 208, 170, 0.3);
- }
-
- .track-info {
- flex: 1;
- }
-
- .track-name {
- font-weight: bold;
- font-size: 16px;
- color: #ffffff;
- }
-
- .track-artist {
- font-size: 14px;
- color: #a0c0d0;
- margin-top: 4px;
- }
-
- .track-time {
- font-size: 12px;
- color: #9fbfe7;
- margin-top: 2px;
- }
-
- .status {
- color: #b9d0aa;
- font-weight: bold;
- }
-
- .loading {
- padding: 20px;
- color: #9fbfe7;
- text-align: center;
- }
-
- .error {
- padding: 20px;
- color: #eca28f;
- text-align: center;
- }
-
- .small {
- font-size: 12px;
- color: #9fbfe7;
- margin-top: 20px;
- }
-
- .small a {
- color: #a0c0d0;
- text-decoration: none;
- }
-
- .small a:hover {
- color: #ffffff;
- text-decoration: underline;
- }
-
- .refresh-btn {
- background: none;
- border: none;
- color: #a0c0d0;
- cursor: pointer;
- font-size: 12px;
- text-decoration: underline;
- }
-
- .refresh-btn:hover {
- color: #ffffff;
- }
-
- .playing-indicator {
- display: inline-block;
- width: 8px;
- height: 8px;
- background: #b9d0aa;
- border-radius: 50%;
- margin-right: 8px;
- animation: pulse 2s infinite;
- }
-
- @keyframes pulse {
- 0% { opacity: 1; }
- 50% { opacity: 0.3; }
- 100% { opacity: 1; }
- }
- </style>
-</head>
-<body>
- <div class="container">
- <ul>
- <li class="success">Now Playing</li>
- </ul>
-
- <h1>Recent Tracks</h1>
- <ul id="tracks-container"
- hx-get="/api/1/user/mtmn/listens"
- hx-trigger="load, every 30s"
- hx-target="#tracks-container"
- hx-ext="listenbrainz">
- <li class="loading">Loading recent tracks...</li>
- </ul>
-
- <p class="small">
- <button class="refresh-btn" hx-get="/api/1/user/mtmn/listens" hx-target="#tracks-container">Refresh</button> •
- <a href="https://listenbrainz.org/user/mtmn/">ListenBrainz</a> •
- </p>
-
- <p class="small" id="last-updated"></p>
- </div>
-
- <script>
- // Update last updated time
- function updateLastUpdated() {
- const now = new Date();
- document.getElementById('last-updated').textContent = `Last check: ${now.toISOString()}`;
- }
-
- // Handle ListenBrainz API response
- htmx.defineExtension('listenbrainz', {
- onEvent: function(name, evt) {
- if (name === 'htmx:afterRequest') {
- const xhr = evt.detail.xhr;
- if (xhr.status === 200) {
- try {
- const data = JSON.parse(xhr.responseText);
- if (data.payload && data.payload.listens) {
- const html = generateTracksHTML(data.payload.listens);
- document.getElementById('tracks-container').innerHTML = html;
- } else {
- document.getElementById('tracks-container').innerHTML = '<li class="error">No tracks found</li>';
- }
- } catch (e) {
- document.getElementById('tracks-container').innerHTML = '<li class="error">Error parsing response</li>';
- }
- } else {
- document.getElementById('tracks-container').innerHTML = '<li class="error">Error loading tracks</li>';
- }
- updateLastUpdated();
- }
- }
- });
-
- function generateTracksHTML(listens) {
- let html = '';
- listens.forEach((listen, index) => {
- const track = listen.track_metadata;
- const listenedAt = formatTimeAgo(listen.listened_at);
-
- const trackName = track.track_name || 'Unknown Track';
- const artistName = track.artist_name || 'Unknown Artist';
- const releaseName = track.release_name || 'Unknown Album';
-
- const playingIndicator = index === 0 ? '<span class="playing-indicator"></span>' : '';
-
- html += `
- <li class="success">
- <div class="track-info">
- <div class="track-name">${playingIndicator}${escapeHtml(trackName)}</div>
- <div class="track-artist">${escapeHtml(artistName)}</div>
- <div class="track-time">${escapeHtml(releaseName)} • ${listenedAt}</div>
- </div>
- <span class="status">Played</span>
- </li>
- `;
- });
- return html;
- }
-
- function formatTimeAgo(timestamp) {
- const now = Math.floor(Date.now() / 1000);
- const diff = now - timestamp;
-
- if (diff < 60) {
- return 'just now';
- } else if (diff < 3600) {
- const minutes = Math.floor(diff / 60);
- return `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
- } else if (diff < 86400) {
- const hours = Math.floor(diff / 3600);
- return `${hours} hour${hours > 1 ? 's' : ''} ago`;
- } else {
- const days = Math.floor(diff / 86400);
- return `${days} day${days > 1 ? 's' : ''} ago`;
- }
- }
-
- function escapeHtml(text) {
- const map = {
- '&': '&',
- '<': '<',
- '>': '>',
- '"': '"',
- "'": '''
- };
- return text.replace(/[&<>"']/g, m => map[m]);
- }
-
- // Initial update
- updateLastUpdated();
- </script>
-</body>
-</html>
diff --git a/package-lock.json.REMOVED.git-id b/package-lock.json.REMOVED.git-id
@@ -1 +1 @@
-cb42bac2fd6f70dc9423a238f450ac6dc2ad8a2d
-\ No newline at end of file
+0906d25db792f3ec30afece36314f061a3baf45a
+\ No newline at end of file
diff --git a/package.json b/package.json
@@ -3,19 +3,26 @@
"version": "1.0.0",
"description": "ListenBrainz scrobbler in PureScript",
"main": "index.js",
+ "type": "module",
"scripts": {
- "build": "npx spago build",
+ "build": "npx spago build && npx esbuild output/Main/index.js --bundle --platform=node --format=esm --outfile=server.js --external:http --external:https --external:xhr2 --external:dotenv --external:url && npx spago bundle --module Client --outfile client.js --platform browser",
+ "build-client": "npx spago bundle --module Client --outfile client.js --platform browser",
+ "build-server": "npx esbuild output/Main/index.js --bundle --platform=node --format=esm --outfile=server.js --external:http --external:https --external:xhr2 --external:dotenv --external:url",
"test": "npx spago test",
"tidy": "purs-tidy format-in-place src/**/*.purs",
- "serve": "npx spago build && node -e \"require('spago').run({cmd: 'bundle-app', main: 'Main', to: './index.js', platform: 'node'})\" && node ./index.js",
- "start": "npx spago build && node -e \"require('spago').run({cmd: 'bundle-app', main: 'Main', to: './index.js', platform: 'node'})\" && node ./index.js"
+ "serve": "node server.js 2>&1 | tee server.log",
+ "start": "node server.js 2>&1 | tee server.log"
},
"devDependencies": {
"esbuild": "^0.28.0",
- "purescript": "^0.15.15",
+ "purescript": "^0.15.16",
"purescript-language-server": "^0.18.0",
"purescript-psa": "^0.9.0",
"purs-tidy": "^0.11.1",
- "spago": "^0.21.0"
+ "spago": "^1.0.4"
+ },
+ "dependencies": {
+ "dotenv": "^17.4.1",
+ "xhr2": "^0.2.1"
}
}
diff --git a/packages.dhall b/packages.dhall
@@ -1,105 +0,0 @@
-{-
-Welcome to your new Dhall package-set!
-
-Below are instructions for how to edit this file for most use
-cases, so that you don't need to know Dhall to use it.
-
-## Use Cases
-
-Most will want to do one or both of these options:
-1. Override/Patch a package's dependency
-2. Add a package not already in the default package set
-
-This file will continue to work whether you use one or both options.
-Instructions for each option are explained below.
-
-### Overriding/Patching a package
-
-Purpose:
-- Change a package's dependency to a newer/older release than the
- default package set's release
-- Use your own modified version of some dependency that may
- include new API, changed API, removed API by
- using your custom git repo of the library rather than
- the package set's repo
-
-Syntax:
-where `entityName` is one of the following:
-- dependencies
-- repo
-- version
--------------------------------
-let upstream = --
-in upstream
- with packageName.entityName = "new value"
--------------------------------
-
-Example:
--------------------------------
-let upstream = --
-in upstream
- with halogen.version = "master"
- with halogen.repo = "https://example.com/path/to/git/repo.git"
-
- with halogen-vdom.version = "v4.0.0"
- with halogen-vdom.dependencies = [ "extra-dependency" ] # halogen-vdom.dependencies
--------------------------------
-
-### Additions
-
-Purpose:
-- Add packages that aren't already included in the default package set
-
-Syntax:
-where `<version>` is:
-- a tag (i.e. "v4.0.0")
-- a branch (i.e. "master")
-- commit hash (i.e. "701f3e44aafb1a6459281714858fadf2c4c2a977")
--------------------------------
-let upstream = --
-in upstream
- with new-package-name =
- { dependencies =
- [ "dependency1"
- , "dependency2"
- ]
- , repo =
- "https://example.com/path/to/git/repo.git"
- , version =
- "<version>"
- }
--------------------------------
-
-Example:
--------------------------------
-let upstream = --
-in upstream
- with benchotron =
- { dependencies =
- [ "arrays"
- , "exists"
- , "profunctor"
- , "strings"
- , "quickcheck"
- , "lcg"
- , "transformers"
- , "foldable-traversable"
- , "exceptions"
- , "node-fs"
- , "node-buffer"
- , "node-readline"
- , "datetime"
- , "now"
- ]
- , repo =
- "https://github.com/hdgarrood/purescript-benchotron.git"
- , version =
- "v7.0.0"
- }
--------------------------------
--}
-let upstream =
- https://github.com/purescript/package-sets/releases/download/psc-0.15.15-20260410/packages.dhall
- sha256:790bcce8148c65d74e13a149145e5a26869d4d625c29093cb4ac0883d0cd3e05
-
-in upstream
-\ No newline at end of file
diff --git a/server.log b/server.log
diff --git a/spago.dhall b/spago.dhall
@@ -1,18 +0,0 @@
-{ name = "scrobbler.purs"
-, dependencies =
- [ "aff"
- , "console"
- , "effect"
- , "either"
- , "exceptions"
- , "node-buffer"
- , "node-event-emitter"
- , "node-http"
- , "node-net"
- , "node-streams"
- , "prelude"
- , "unsafe-coerce"
- ]
-, packages = ./packages.dhall
-, sources = [ "src/**/*.purs" ]
-}
diff --git a/spago.lock b/spago.lock
@@ -0,0 +1,2168 @@
+{
+ "workspace": {
+ "packages": {
+ "scrobbler-purescript": {
+ "path": "./",
+ "core": {
+ "dependencies": [
+ "aff",
+ "affjax",
+ "affjax-web",
+ "argonaut",
+ "argonaut-core",
+ "arrays",
+ "console",
+ "datetime",
+ "effect",
+ "either",
+ "exceptions",
+ "fetch",
+ "fetch-argonaut",
+ "foreign-object",
+ "halogen",
+ "http-methods",
+ "integers",
+ "js-uri",
+ "maybe",
+ "node-buffer",
+ "node-event-emitter",
+ "node-fs",
+ "node-http",
+ "node-net",
+ "node-process",
+ "node-streams",
+ "node-url",
+ "now",
+ "ordered-collections",
+ "prelude",
+ "strings",
+ "tailrec",
+ "unsafe-coerce",
+ "web-dom"
+ ]
+ },
+ "test": {
+ "dependencies": []
+ }
+ }
+ },
+ "package_set": {
+ "address": {
+ "registry": "75.9.0"
+ },
+ "compiler": ">=0.15.15 <0.16.0",
+ "content": {
+ "abc-parser": "2.1.0",
+ "ace": "9.1.0",
+ "address-rfc2821": "0.1.1",
+ "aff": "8.0.0",
+ "aff-bus": "6.0.0",
+ "aff-coroutines": "9.0.0",
+ "aff-promise": "4.0.0",
+ "aff-retry": "2.0.0",
+ "affjax": "13.0.0",
+ "affjax-node": "1.0.0",
+ "affjax-web": "1.0.0",
+ "ansi": "7.0.0",
+ "apexcharts": "0.5.0",
+ "applicative-phases": "1.0.0",
+ "argonaut": "9.0.0",
+ "argonaut-aeson-generic": "0.4.1",
+ "argonaut-codecs": "9.1.0",
+ "argonaut-core": "7.0.0",
+ "argonaut-generic": "8.0.0",
+ "argonaut-traversals": "10.0.0",
+ "argparse-basic": "2.0.0",
+ "array-builder": "0.1.2",
+ "array-search": "0.6.0",
+ "arraybuffer": "13.2.0",
+ "arraybuffer-builder": "3.1.0",
+ "arraybuffer-types": "3.0.2",
+ "arrays": "7.3.0",
+ "arrays-extra": "0.6.1",
+ "arrays-zipper": "2.0.1",
+ "ask": "1.0.0",
+ "assert": "6.0.0",
+ "assert-multiple": "0.4.0",
+ "avar": "5.0.1",
+ "axon": "0.0.3",
+ "b64": "0.0.8",
+ "barbies": "1.0.1",
+ "barlow-lens": "0.9.0",
+ "benchlib": "0.0.4",
+ "bifunctors": "6.1.0",
+ "bigints": "7.0.1",
+ "blessed": "1.0.0",
+ "bolson": "0.3.9",
+ "bookhound": "0.1.7",
+ "bower-json": "3.0.0",
+ "call-by-name": "4.0.1",
+ "canvas": "6.0.0",
+ "canvas-action": "9.0.0",
+ "cartesian": "1.0.6",
+ "catenable-lists": "7.0.0",
+ "cbor-stream": "1.3.0",
+ "chameleon": "1.0.0",
+ "chameleon-halogen": "1.0.3",
+ "chameleon-react-basic": "1.1.0",
+ "chameleon-styled": "2.5.0",
+ "chameleon-transformers": "1.0.0",
+ "channel": "1.0.0",
+ "chartjs": "0.1.1",
+ "chartjs-halogen": "0.1.0",
+ "checked-exceptions": "3.1.1",
+ "choku": "1.0.2",
+ "classless": "0.1.1",
+ "classless-arbitrary": "0.1.1",
+ "classless-decode-json": "0.1.1",
+ "classless-encode-json": "0.1.3",
+ "classnames": "2.0.0",
+ "codec": "6.1.0",
+ "codec-argonaut": "10.0.0",
+ "codec-json": "2.0.0",
+ "colors": "7.0.1",
+ "compile-fail": "0.4.0",
+ "concur-core": "0.5.0",
+ "concur-react": "0.5.0",
+ "concurrent-queues": "3.0.0",
+ "console": "6.1.0",
+ "const": "6.0.0",
+ "contravariant": "6.0.0",
+ "control": "6.0.0",
+ "convertable-options": "1.0.0",
+ "coroutines": "7.0.0",
+ "css": "6.0.0",
+ "css-class-name-extractor": "0.0.4",
+ "css-frameworks": "1.1.0",
+ "csv-stream": "2.3.0",
+ "currency": "0.1.0",
+ "data-mvc": "0.0.2",
+ "datetime": "6.1.0",
+ "datetime-parsing": "0.2.0",
+ "debounce": "0.1.0",
+ "debug": "6.0.2",
+ "decimals": "7.1.0",
+ "default-values": "1.0.1",
+ "deku": "0.9.23",
+ "deno": "0.0.5",
+ "dissect": "1.0.0",
+ "distributive": "6.0.0",
+ "dodo-printer": "2.2.3",
+ "dom-filereader": "7.0.0",
+ "dom-indexed": "13.0.0",
+ "dom-simple": "0.4.0",
+ "dotenv": "4.0.3",
+ "droplet": "0.6.0",
+ "dts": "1.0.0",
+ "dual-numbers": "1.0.3",
+ "dynamic-buffer": "3.0.1",
+ "echarts-simple": "0.0.1",
+ "effect": "4.0.0",
+ "either": "6.1.0",
+ "elmish": "0.13.0",
+ "elmish-enzyme": "0.1.1",
+ "elmish-hooks": "0.11.0",
+ "elmish-html": "0.10.0",
+ "elmish-testing-library": "0.3.2",
+ "elmish-time-machine": "0.4.2",
+ "email-validate": "7.0.0",
+ "encoding": "0.0.9",
+ "enums": "6.0.1",
+ "env-names": "0.4.0",
+ "environment": "2.0.0",
+ "error": "2.0.0",
+ "eta-conversion": "0.3.2",
+ "exceptions": "6.1.0",
+ "exists": "6.0.0",
+ "exitcodes": "4.0.0",
+ "expect-inferred": "3.0.0",
+ "express": "0.9.1",
+ "ezfetch": "1.1.1",
+ "fahrtwind": "2.0.0",
+ "faker-ffi": "0.1.1",
+ "fakerjs": "0.0.1",
+ "fallback": "0.1.0",
+ "fast-vect": "1.2.0",
+ "fetch": "4.1.0",
+ "fetch-argonaut": "1.0.1",
+ "fetch-core": "5.1.0",
+ "fetch-yoga-json": "1.1.0",
+ "ffi-simple": "0.5.1",
+ "fft": "0.3.0",
+ "fft-js": "0.1.0",
+ "filterable": "5.0.0",
+ "fix-functor": "0.1.0",
+ "fixed-points": "7.0.0",
+ "fixed-precision": "5.0.0",
+ "flame": "1.6.0",
+ "float32": "2.0.0",
+ "fmt": "0.2.1",
+ "foldable-traversable": "6.0.0",
+ "foldable-traversable-extra": "0.0.6",
+ "foreign": "7.0.0",
+ "foreign-object": "4.1.0",
+ "foreign-readwrite": "3.4.0",
+ "forgetmenot": "0.1.0",
+ "fork": "6.0.0",
+ "form-urlencoded": "7.0.0",
+ "formatters": "7.0.0",
+ "framer-motion": "3.1.0",
+ "free": "7.1.0",
+ "freeap": "7.0.0",
+ "freer-free": "0.0.1",
+ "freet": "7.0.0",
+ "functions": "6.0.0",
+ "functor1": "3.0.0",
+ "functors": "5.0.0",
+ "fuzzy": "0.4.0",
+ "gen": "4.0.0",
+ "generate-values": "1.0.1",
+ "generic-router": "0.0.1",
+ "geojson": "0.0.5",
+ "geometria": "2.2.0",
+ "gesso": "1.0.1",
+ "gojs": "0.1.1",
+ "golden-test": "0.1.0",
+ "golem-fetch": "0.1.0",
+ "grain": "3.0.0",
+ "grain-router": "3.0.0",
+ "grain-virtualized": "3.0.0",
+ "graphql-client": "10.1.1",
+ "graphs": "8.1.0",
+ "group": "4.1.1",
+ "halogen": "7.0.0",
+ "halogen-bootstrap5": "5.3.2",
+ "halogen-canvas": "1.0.0",
+ "halogen-css": "10.0.0",
+ "halogen-declarative-canvas": "0.0.8",
+ "halogen-echarts-simple": "0.0.4",
+ "halogen-formless": "4.0.3",
+ "halogen-helix": "1.1.0",
+ "halogen-hooks": "0.6.3",
+ "halogen-hooks-extra": "0.9.0",
+ "halogen-infinite-scroll": "1.1.0",
+ "halogen-store": "0.5.4",
+ "halogen-storybook": "2.0.0",
+ "halogen-subscriptions": "2.0.0",
+ "halogen-svg-elems": "8.0.0",
+ "halogen-typewriter": "1.0.4",
+ "halogen-use-trigger-hooks": "1.0.0",
+ "halogen-vdom": "8.0.0",
+ "halogen-vdom-string-renderer": "0.5.0",
+ "halogen-xterm": "2.0.0",
+ "heckin": "2.0.1",
+ "heterogeneous": "0.7.0",
+ "homogeneous": "0.4.0",
+ "http-methods": "6.0.0",
+ "httpurple": "4.0.0",
+ "huffman": "0.4.0",
+ "humdrum": "0.0.1",
+ "hylograph-canvas": "0.1.0",
+ "hylograph-d3-kernel": "0.1.0",
+ "hylograph-graph": "0.3.0",
+ "hylograph-layout": "0.3.0",
+ "hylograph-music": "0.4.0",
+ "hylograph-optics": "0.1.0",
+ "hylograph-selection": "0.5.1",
+ "hylograph-simulation": "0.5.0",
+ "hylograph-simulation-core": "0.1.0",
+ "hylograph-simulation-halogen": "0.5.0",
+ "hylograph-transitions": "0.1.0",
+ "hylograph-wasm-kernel": "0.1.0",
+ "hyrule": "2.3.8",
+ "i18next": "0.1.0",
+ "idb": "0.1.0",
+ "identity": "6.0.0",
+ "identy": "4.0.1",
+ "indexed-db": "1.0.0",
+ "indexed-monad": "3.0.0",
+ "ink": "0.0.1",
+ "int64": "3.0.0",
+ "integers": "6.0.0",
+ "interpolate": "5.0.2",
+ "intersection-observer": "1.0.1",
+ "invariant": "6.0.0",
+ "jarilo": "1.0.1",
+ "jelly": "0.10.0",
+ "jelly-router": "0.3.0",
+ "jelly-signal": "0.4.0",
+ "jest": "1.0.0",
+ "js-abort-controller": "1.0.0",
+ "js-bigints": "2.2.1",
+ "js-date": "8.0.0",
+ "js-fetch": "0.2.1",
+ "js-fileio": "3.0.0",
+ "js-intl": "1.3.0",
+ "js-iterators": "0.1.1",
+ "js-maps": "0.1.2",
+ "js-promise": "1.0.0",
+ "js-promise-aff": "1.0.0",
+ "js-temporal": "2.1.0",
+ "js-timers": "6.1.0",
+ "js-uri": "3.1.0",
+ "jsdom": "1.0.0",
+ "jsinc": "0.5.1",
+ "json": "1.1.0",
+ "json-codecs": "5.0.0",
+ "justifill": "0.5.0",
+ "jwt": "0.0.9",
+ "labeled-data": "0.2.0",
+ "language-cst-parser": "0.14.1",
+ "lazy": "6.0.0",
+ "lazy-joe": "1.0.0",
+ "lcg": "4.0.0",
+ "leibniz": "5.0.0",
+ "leveldb": "1.0.1",
+ "liminal": "1.0.1",
+ "linalg": "6.0.0",
+ "linear": "0.1.0",
+ "lists": "7.0.0",
+ "literals": "1.0.2",
+ "logging": "3.0.0",
+ "logging-journald": "0.4.0",
+ "lumi-components": "18.0.0",
+ "machines": "7.0.0",
+ "maps-eager": "0.5.0",
+ "marionette": "1.0.0",
+ "marionette-react-basic-hooks": "0.1.1",
+ "markdown-it-js": "0.1.0",
+ "marked": "0.1.0",
+ "matrices": "5.0.1",
+ "matryoshka": "1.0.0",
+ "maybe": "6.0.0",
+ "media-types": "6.0.0",
+ "meowclient": "1.0.0",
+ "midi": "4.0.0",
+ "milkdown": "0.1.0",
+ "milkis": "9.0.0",
+ "mimetype": "0.0.1",
+ "minibench": "4.0.1",
+ "mmorph": "7.0.0",
+ "monad-control": "5.0.0",
+ "monad-logger": "1.3.1",
+ "monad-loops": "0.5.0",
+ "monad-unlift": "1.0.1",
+ "monoid-extras": "0.0.1",
+ "monoidal": "0.16.0",
+ "morello": "0.4.0",
+ "mote": "3.0.0",
+ "motsunabe": "2.0.0",
+ "mvc": "0.0.1",
+ "mysql": "6.0.1",
+ "n3": "0.1.0",
+ "nano-id": "1.1.0",
+ "nanoid": "0.1.0",
+ "naturals": "3.0.0",
+ "nested-functor": "0.2.1",
+ "newtype": "5.0.0",
+ "next-purs-rsc": "0.1.0",
+ "nextjs": "0.1.1",
+ "nextui": "0.2.0",
+ "node-buffer": "9.0.0",
+ "node-child-process": "11.1.0",
+ "node-event-emitter": "3.0.0",
+ "node-execa": "5.0.0",
+ "node-fs": "9.2.0",
+ "node-glob-basic": "2.0.0",
+ "node-http": "9.1.0",
+ "node-http2": "1.1.1",
+ "node-human-signals": "1.0.0",
+ "node-net": "5.1.0",
+ "node-os": "5.1.0",
+ "node-path": "5.0.1",
+ "node-process": "11.2.0",
+ "node-readline": "8.1.1",
+ "node-sqlite3": "8.0.0",
+ "node-stream-pipes": "2.1.6",
+ "node-streams": "9.0.1",
+ "node-tls": "0.3.1",
+ "node-url": "7.0.1",
+ "node-workerbees": "0.3.1",
+ "node-zlib": "0.4.0",
+ "nonempty": "7.0.0",
+ "now": "6.0.0",
+ "npm-package-json": "2.0.0",
+ "nqueens": "0.1.4",
+ "nullable": "6.0.0",
+ "numberfield": "0.2.2",
+ "numbers": "9.0.1",
+ "oak": "3.1.1",
+ "oak-debug": "1.2.2",
+ "object-maps": "0.3.0",
+ "ocarina": "1.5.4",
+ "oooooooooorrrrrrrmm-lib": "0.0.1",
+ "open-colors-scales-and-schemes": "1.0.0",
+ "open-drawing": "6.0.4",
+ "open-folds": "6.4.0",
+ "open-foreign-generic": "11.0.3",
+ "open-memoize": "6.2.0",
+ "open-mkdirp-aff": "1.2.0",
+ "open-pairing": "6.2.0",
+ "open-smolder": "12.0.2",
+ "options": "7.0.0",
+ "optparse": "5.0.1",
+ "ordered-collections": "3.2.0",
+ "ordered-set": "0.5.0",
+ "orders": "6.0.0",
+ "org-doc": "0.1.0",
+ "owoify": "1.2.0",
+ "pairs": "9.0.1",
+ "parallel": "7.0.0",
+ "parsing": "11.0.0",
+ "parsing-dataview": "3.2.4",
+ "partial": "4.0.0",
+ "pathy": "9.0.0",
+ "pha": "0.13.0",
+ "phaser": "0.7.0",
+ "phylio": "1.1.2",
+ "pipes": "8.0.0",
+ "pirates-charm": "0.0.1",
+ "play": "1.0.0",
+ "pmock": "0.9.0",
+ "point-free": "1.0.0",
+ "pointed-list": "0.5.1",
+ "polymorphic-vectors": "4.0.0",
+ "posix-types": "6.0.0",
+ "postgresql": "2.0.21",
+ "precise": "6.0.0",
+ "precise-datetime": "7.0.0",
+ "prelude": "6.0.2",
+ "prettier-printer": "3.0.0",
+ "printf": "0.1.0",
+ "priority-queue": "0.1.2",
+ "profunctor": "6.0.1",
+ "profunctor-lenses": "8.0.0",
+ "prospero": "0.1.0",
+ "protobuf": "4.4.0",
+ "psa-utils": "8.0.0",
+ "psci-support": "6.0.0",
+ "punycode": "1.0.0",
+ "pursfmt": "0.17.0",
+ "qualified-do": "2.2.0",
+ "quantities": "12.2.0",
+ "quickcheck": "8.0.1",
+ "quickcheck-combinators": "0.1.3",
+ "quickcheck-laws": "7.0.0",
+ "quickcheck-utf8": "0.0.0",
+ "random": "6.0.0",
+ "rationals": "6.0.0",
+ "rdf": "0.1.0",
+ "react": "11.0.0",
+ "react-aria": "0.2.0",
+ "react-basic": "17.0.0",
+ "react-basic-classic": "3.0.0",
+ "react-basic-dnd": "10.1.0",
+ "react-basic-dom": "7.0.0",
+ "react-basic-dom-beta": "0.1.1",
+ "react-basic-emotion": "7.1.0",
+ "react-basic-hooks": "9.1.1",
+ "react-basic-storybook": "2.0.0",
+ "react-dnd-kit": "0.6.0",
+ "react-dom": "8.0.0",
+ "react-halo": "3.0.0",
+ "react-icons": "1.1.5",
+ "react-markdown": "0.1.0",
+ "react-testing-library": "4.0.1",
+ "react-virtuoso": "1.0.0",
+ "reactix": "0.6.1",
+ "read": "1.0.1",
+ "recharts": "1.1.0",
+ "record": "4.0.0",
+ "record-extra": "5.0.1",
+ "record-extra-srghma": "0.2.8",
+ "record-ptional-fields": "0.1.2",
+ "record-studio": "1.0.4",
+ "ref-lifted": "0.0.2",
+ "refs": "6.0.0",
+ "remotedata": "5.0.1",
+ "repr": "0.5.0",
+ "resize-arrays": "0.0.1",
+ "resize-observer": "1.0.0",
+ "resource": "2.0.1",
+ "resourcet": "1.0.0",
+ "result": "1.0.3",
+ "return": "0.2.0",
+ "ring-modules": "5.0.1",
+ "rito": "0.3.4",
+ "roman": "0.4.0",
+ "rough-notation": "1.0.2",
+ "routing": "11.0.0",
+ "routing-duplex": "0.7.0",
+ "run": "5.0.0",
+ "safe-coerce": "2.0.0",
+ "safely": "4.0.1",
+ "school-of-music": "1.3.0",
+ "selection-foldable": "0.2.0",
+ "selective-functors": "1.0.1",
+ "semirings": "7.0.0",
+ "shuffle": "2.0.0",
+ "sigil": "0.3.0",
+ "sigil-hats": "0.3.0",
+ "signal": "13.0.0",
+ "simple-emitter": "3.0.1",
+ "simple-i18n": "2.0.1",
+ "simple-json": "9.0.0",
+ "simple-json-generics": "0.2.1",
+ "simple-ulid": "3.0.0",
+ "sized-matrices": "1.0.0",
+ "sized-vectors": "5.0.2",
+ "slug": "3.2.0",
+ "small-ffi": "4.0.1",
+ "soundfonts": "4.1.0",
+ "sparse-matrices": "2.0.1",
+ "sparse-polynomials": "3.0.1",
+ "spec": "8.1.2",
+ "spec-discovery": "8.4.1",
+ "spec-mocha": "5.1.1",
+ "spec-node": "0.0.3",
+ "spec-quickcheck": "5.0.2",
+ "spec-reporter-xunit": "0.7.1",
+ "splitmix": "2.1.0",
+ "ssrs": "1.0.0",
+ "st": "6.2.0",
+ "statistics": "0.3.2",
+ "strictlypositiveint": "1.0.1",
+ "string-parsers": "8.0.0",
+ "strings": "6.0.1",
+ "strings-extra": "4.0.0",
+ "stringutils": "0.0.12",
+ "structured-logging": "1.0.0",
+ "substitute": "0.2.3",
+ "supply": "0.2.0",
+ "svg-parser": "3.0.0",
+ "systemd-journald": "0.3.0",
+ "tagged": "4.0.2",
+ "tailrec": "6.1.0",
+ "tanstack-query": "2.0.1",
+ "tecton": "0.2.1",
+ "tecton-halogen": "0.2.0",
+ "temporal": "0.1.0",
+ "test-unit": "17.0.0",
+ "text-formatting": "0.1.0",
+ "thermite": "6.3.1",
+ "thermite-dom": "0.3.1",
+ "these": "6.0.0",
+ "threading": "0.0.3",
+ "tidy": "0.11.1",
+ "tidy-codegen": "4.0.1",
+ "tldr": "0.0.0",
+ "toestand": "0.9.0",
+ "transformation-matrix": "1.0.1",
+ "transformers": "6.1.0",
+ "transit": "1.0.0",
+ "tree-rose": "4.0.2",
+ "trivial-unfold": "0.5.0",
+ "ts-bridge": "4.0.0",
+ "tuples": "7.0.0",
+ "two-or-more": "1.0.0",
+ "type-equality": "4.0.1",
+ "typedenv": "2.0.1",
+ "typelevel": "6.0.0",
+ "typelevel-lists": "2.1.0",
+ "typelevel-peano": "1.0.1",
+ "typelevel-prelude": "7.0.0",
+ "typelevel-regex": "0.0.3",
+ "typelevel-rows": "0.1.0",
+ "typisch": "0.4.0",
+ "uint": "7.0.0",
+ "ulid": "3.0.1",
+ "uncurried-transformers": "1.1.0",
+ "undefined": "2.0.0",
+ "undefined-is-not-a-problem": "1.1.0",
+ "unfoldable": "6.0.0",
+ "unicode": "6.0.0",
+ "unique": "0.6.1",
+ "unlift": "1.0.1",
+ "unordered-collections": "3.1.0",
+ "unsafe-coerce": "6.0.0",
+ "unsafe-reference": "5.0.0",
+ "untagged-to-tagged": "0.1.4",
+ "untagged-union": "1.0.0",
+ "uri": "9.0.0",
+ "url-immutable": "1.0.0",
+ "url-regex-safe": "0.1.1",
+ "uuid": "9.0.0",
+ "uuidv4": "1.0.0",
+ "validation": "6.0.0",
+ "variant": "8.0.0",
+ "variant-encodings": "2.0.0",
+ "variant-gen": "1.0.0",
+ "vectorfield": "1.0.1",
+ "vectors": "2.1.0",
+ "versions": "7.0.0",
+ "visx": "0.0.2",
+ "vitest": "2.0.0",
+ "web-chain": "1.0.1",
+ "web-clipboard": "6.0.0",
+ "web-cssom": "2.0.0",
+ "web-cssom-view": "0.1.0",
+ "web-dom": "6.0.0",
+ "web-dom-parser": "8.0.0",
+ "web-dom-xpath": "3.0.0",
+ "web-encoding": "3.0.0",
+ "web-events": "4.0.0",
+ "web-fetch": "4.0.1",
+ "web-file": "4.0.0",
+ "web-geometry": "0.1.0",
+ "web-html": "4.1.1",
+ "web-pointerevents": "2.0.0",
+ "web-proletarian": "1.0.0",
+ "web-promise": "3.2.0",
+ "web-resize-observer": "2.1.0",
+ "web-router": "1.0.0",
+ "web-socket": "4.0.0",
+ "web-storage": "5.0.0",
+ "web-streams": "4.0.0",
+ "web-touchevents": "4.0.0",
+ "web-uievents": "5.0.0",
+ "web-url": "2.0.0",
+ "web-workers": "1.1.0",
+ "web-xhr": "5.0.1",
+ "webb-aff-list": "0.0.2",
+ "webb-array": "0.0.2",
+ "webb-channel": "0.0.3",
+ "webb-commandline": "0.0.3",
+ "webb-directory": "0.0.7",
+ "webb-file": "0.0.1",
+ "webb-map": "0.0.1",
+ "webb-monad": "0.0.2",
+ "webb-mutex": "0.0.7",
+ "webb-parsing": "0.0.5",
+ "webb-random": "0.0.1",
+ "webb-refer": "0.0.3",
+ "webb-set": "0.0.1",
+ "webb-slot": "0.0.1",
+ "webb-stateful": "0.0.3",
+ "webb-string": "0.0.1",
+ "webb-test": "0.0.1",
+ "webb-thread": "0.0.2",
+ "webb-writer": "0.0.3",
+ "webextension-polyfill": "0.1.0",
+ "webgpu": "0.0.1",
+ "which": "2.0.0",
+ "whine-core": "0.0.33",
+ "xterm": "1.0.0",
+ "yaml-next": "3.1.1",
+ "yoga-acp-om": "0.1.1",
+ "yoga-better-auth": "0.2.2",
+ "yoga-bun-yoga": "0.2.1",
+ "yoga-config": "0.2.1",
+ "yoga-country": "3.0.0",
+ "yoga-docker-compose": "0.1.1",
+ "yoga-dynamodb": "0.1.1",
+ "yoga-elasticsearch": "0.1.1",
+ "yoga-fastify": "0.5.1",
+ "yoga-fastify-om": "0.4.4",
+ "yoga-fetch": "1.0.1",
+ "yoga-fetch-om": "0.6.2",
+ "yoga-http-api": "0.3.1",
+ "yoga-jaeger": "0.1.1",
+ "yoga-json": "5.2.1",
+ "yoga-language": "1.0.0",
+ "yoga-next-fastify": "0.1.1",
+ "yoga-om": "2.2.0",
+ "yoga-om-layer": "2.1.0",
+ "yoga-om-strom": "0.4.2",
+ "yoga-om-workerbees": "0.1.2",
+ "yoga-opentelemetry": "0.2.0",
+ "yoga-options": "0.1.1",
+ "yoga-pino": "0.1.1",
+ "yoga-postgres": "6.0.0",
+ "yoga-react": "1.0.2",
+ "yoga-react-dom": "2.0.1",
+ "yoga-react-native": "0.2.1",
+ "yoga-react-om": "0.2.0",
+ "yoga-redis": "0.1.0",
+ "yoga-shadcn": "1.0.5",
+ "yoga-sql-types": "0.1.0",
+ "yoga-sqlite": "0.3.0",
+ "yoga-sse": "0.1.1",
+ "yoga-subtlecrypto": "0.1.0",
+ "yoga-test-docker": "0.1.2",
+ "yoga-timezone": "1.0.0",
+ "yoga-tree": "1.0.0",
+ "yoga-tree-svg": "0.1.0",
+ "yoga-tree-utils": "1.0.0",
+ "z3": "0.0.2",
+ "zipperarray": "2.0.0"
+ }
+ },
+ "extra_packages": {}
+ },
+ "packages": {
+ "aff": {
+ "type": "registry",
+ "version": "8.0.0",
+ "integrity": "sha256-9BRIrSdRoYybfiUmBqgKJ66cPqxYHb277bVWQMn/9sg=",
+ "dependencies": [
+ "control",
+ "datetime",
+ "effect",
+ "either",
+ "exceptions",
+ "functions",
+ "newtype",
+ "parallel",
+ "partial",
+ "prelude",
+ "st",
+ "tailrec",
+ "transformers",
+ "unsafe-coerce"
+ ]
+ },
+ "affjax": {
+ "type": "registry",
+ "version": "13.0.0",
+ "integrity": "sha256-dpOV8ELNqUVBwxE6eKFdnFky+sLwPFANrFUzIxQablk=",
+ "dependencies": [
+ "aff",
+ "argonaut-core",
+ "arraybuffer-types",
+ "arrays",
+ "control",
+ "datetime",
+ "either",
+ "exceptions",
+ "foldable-traversable",
+ "foreign",
+ "form-urlencoded",
+ "functions",
+ "http-methods",
+ "lists",
+ "maybe",
+ "media-types",
+ "newtype",
+ "nullable",
+ "prelude",
+ "transformers",
+ "web-dom",
+ "web-file",
+ "web-xhr"
+ ]
+ },
+ "affjax-web": {
+ "type": "registry",
+ "version": "1.0.0",
+ "integrity": "sha256-sHg+G50LDoZYG6UHvUiH1ZLT/UHIXaEF5DUBaPXxtxM=",
+ "dependencies": [
+ "aff",
+ "affjax",
+ "either",
+ "maybe",
+ "prelude"
+ ]
+ },
+ "argonaut": {
+ "type": "registry",
+ "version": "9.0.0",
+ "integrity": "sha256-M/EUEihkRu5xKGKe7UfkrybGj56SDTKz1EappEOiWgA=",
+ "dependencies": [
+ "argonaut-codecs",
+ "argonaut-core",
+ "argonaut-traversals"
+ ]
+ },
+ "argonaut-codecs": {
+ "type": "registry",
+ "version": "9.1.0",
+ "integrity": "sha256-K910SBrmYallESsm8pxJYThs7lig6hxX3M4PW33Cgew=",
+ "dependencies": [
+ "argonaut-core",
+ "arrays",
+ "bifunctors",
+ "either",
+ "foldable-traversable",
+ "foreign-object",
+ "identity",
+ "integers",
+ "lists",
+ "maybe",
+ "nonempty",
+ "ordered-collections",
+ "prelude",
+ "record",
+ "strings",
+ "tuples"
+ ]
+ },
+ "argonaut-core": {
+ "type": "registry",
+ "version": "7.0.0",
+ "integrity": "sha256-gYZihtBIKthVuXXoiPphMookSf8M5aHyFYkcIWqlxTM=",
+ "dependencies": [
+ "arrays",
+ "control",
+ "either",
+ "foreign-object",
+ "functions",
+ "gen",
+ "maybe",
+ "nonempty",
+ "prelude",
+ "strings",
+ "tailrec"
+ ]
+ },
+ "argonaut-traversals": {
+ "type": "registry",
+ "version": "10.0.0",
+ "integrity": "sha256-guTCBwDiOECrV9CvHF2cH61wa0xMNkbw2/zgl9c0QVk=",
+ "dependencies": [
+ "argonaut-codecs",
+ "argonaut-core",
+ "arrays",
+ "control",
+ "either",
+ "foldable-traversable",
+ "foreign-object",
+ "gen",
+ "integers",
+ "lists",
+ "maybe",
+ "prelude",
+ "profunctor-lenses",
+ "strings",
+ "tailrec",
+ "tuples",
+ "unfoldable"
+ ]
+ },
+ "arraybuffer-types": {
+ "type": "registry",
+ "version": "3.0.2",
+ "integrity": "sha256-p05cJnSkyeoB7VHzMyc2Eb1RwUaB7Nl0sQSqEGNEUD8=",
+ "dependencies": []
+ },
+ "arrays": {
+ "type": "registry",
+ "version": "7.3.0",
+ "integrity": "sha256-GD4z7LCi9wzi7FCQqbWhvs8paoUK9NO+HwgXZ+KP8Rk=",
+ "dependencies": [
+ "bifunctors",
+ "control",
+ "foldable-traversable",
+ "functions",
+ "maybe",
+ "nonempty",
+ "partial",
+ "prelude",
+ "safe-coerce",
+ "st",
+ "tailrec",
+ "tuples",
+ "unfoldable",
+ "unsafe-coerce"
+ ]
+ },
+ "bifunctors": {
+ "type": "registry",
+ "version": "6.1.0",
+ "integrity": "sha256-CvZRb8uI75eKIqiYntR+k05Wk6tKlMS5w8sCA2AFna0=",
+ "dependencies": [
+ "const",
+ "either",
+ "newtype",
+ "prelude",
+ "tuples"
+ ]
+ },
+ "catenable-lists": {
+ "type": "registry",
+ "version": "7.0.0",
+ "integrity": "sha256-7+i/MHBLIRh604iCNT4ENWRWE/wuHIotER4kpdX9Ve0=",
+ "dependencies": [
+ "control",
+ "foldable-traversable",
+ "lists",
+ "maybe",
+ "prelude",
+ "tuples",
+ "unfoldable"
+ ]
+ },
+ "console": {
+ "type": "registry",
+ "version": "6.1.0",
+ "integrity": "sha256-3T5PPz1mATZmV0C4GzkCkpLlCa5xG7u8ZNgnLrrrI1Q=",
+ "dependencies": [
+ "effect",
+ "prelude"
+ ]
+ },
+ "const": {
+ "type": "registry",
+ "version": "6.0.0",
+ "integrity": "sha256-S+m0lQsxnji0z1XRUmkn+3RFuWJ5CXWuIr1u3i8Bfoc=",
+ "dependencies": [
+ "invariant",
+ "newtype",
+ "prelude"
+ ]
+ },
+ "contravariant": {
+ "type": "registry",
+ "version": "6.0.0",
+ "integrity": "sha256-nuckEWqL80NaPmR/l9AUyrfycNh18XcvlZV9PAamwqc=",
+ "dependencies": [
+ "const",
+ "either",
+ "newtype",
+ "prelude",
+ "tuples"
+ ]
+ },
+ "control": {
+ "type": "registry",
+ "version": "6.0.0",
+ "integrity": "sha256-0vxyYh5FL+ilxgLo954w1LqCUC0RSIivNNlb9CaNUQg=",
+ "dependencies": [
+ "newtype",
+ "prelude"
+ ]
+ },
+ "datetime": {
+ "type": "registry",
+ "version": "6.1.0",
+ "integrity": "sha256-tbv6eDXy6QOD7YSknxYSbsDF32OC2JrNbSBpzMs7Doc=",
+ "dependencies": [
+ "bifunctors",
+ "control",
+ "either",
+ "enums",
+ "foldable-traversable",
+ "functions",
+ "gen",
+ "integers",
+ "lists",
+ "maybe",
+ "newtype",
+ "numbers",
+ "ordered-collections",
+ "partial",
+ "prelude",
+ "tuples"
+ ]
+ },
+ "distributive": {
+ "type": "registry",
+ "version": "6.0.0",
+ "integrity": "sha256-netyJK4B32PDYHD0tF2AenKgo3urzbI770ycM0pArAo=",
+ "dependencies": [
+ "identity",
+ "newtype",
+ "prelude",
+ "tuples",
+ "type-equality"
+ ]
+ },
+ "dom-indexed": {
+ "type": "registry",
+ "version": "13.0.0",
+ "integrity": "sha256-Tr9uDQdmKN4TUGMg1/4KzsVu5zPa8OqKB4N6/S2a8vU=",
+ "dependencies": [
+ "datetime",
+ "media-types",
+ "prelude",
+ "strings",
+ "web-clipboard",
+ "web-events",
+ "web-html",
+ "web-pointerevents",
+ "web-touchevents",
+ "web-uievents"
+ ]
+ },
+ "effect": {
+ "type": "registry",
+ "version": "4.0.0",
+ "integrity": "sha256-DdqU3bncRTjgMVPPRlAuGHVeajSPEcRaRhNuio7if6s=",
+ "dependencies": [
+ "prelude"
+ ]
+ },
+ "either": {
+ "type": "registry",
+ "version": "6.1.0",
+ "integrity": "sha256-tBHx3PgtH4GZOApZCDkcM7szRTYJinrUrVGWazQCUUg=",
+ "dependencies": [
+ "control",
+ "invariant",
+ "maybe",
+ "prelude"
+ ]
+ },
+ "enums": {
+ "type": "registry",
+ "version": "6.0.1",
+ "integrity": "sha256-sdZOmLX5+5pASGpvVyT0vSD5BBYQjZiayjV5XiPutso=",
+ "dependencies": [
+ "control",
+ "either",
+ "gen",
+ "maybe",
+ "newtype",
+ "nonempty",
+ "partial",
+ "prelude",
+ "tuples",
+ "unfoldable"
+ ]
+ },
+ "exceptions": {
+ "type": "registry",
+ "version": "6.1.0",
+ "integrity": "sha256-nRrr+N0wk0TUFOWLR7e1n1oxcXo/X345bQUtoY9lW6A=",
+ "dependencies": [
+ "effect",
+ "either",
+ "maybe",
+ "prelude"
+ ]
+ },
+ "exists": {
+ "type": "registry",
+ "version": "6.0.0",
+ "integrity": "sha256-vtLbrWNaI+pzx/2fw2AQnCovoLtcosClIhjO26QKkh8=",
+ "dependencies": [
+ "unsafe-coerce"
+ ]
+ },
+ "fetch": {
+ "type": "registry",
+ "version": "4.1.0",
+ "integrity": "sha256-AY3VFeK1Y0Mc6Gsr0DCXlbAn4MnhCXFW48EJW8XMdDI=",
+ "dependencies": [
+ "aff",
+ "arraybuffer-types",
+ "bifunctors",
+ "effect",
+ "either",
+ "foreign",
+ "http-methods",
+ "js-fetch",
+ "js-promise",
+ "js-promise-aff",
+ "maybe",
+ "newtype",
+ "ordered-collections",
+ "prelude",
+ "record",
+ "strings",
+ "typelevel-prelude",
+ "web-file",
+ "web-streams"
+ ]
+ },
+ "fetch-argonaut": {
+ "type": "registry",
+ "version": "1.0.1",
+ "integrity": "sha256-Gn9CCRtTpar9jKJuDyPODFnPA9NE6bhwt/HkcDL/bs0=",
+ "dependencies": [
+ "aff",
+ "argonaut",
+ "either",
+ "foreign",
+ "prelude",
+ "transformers",
+ "unsafe-coerce"
+ ]
+ },
+ "foldable-traversable": {
+ "type": "registry",
+ "version": "6.0.0",
+ "integrity": "sha256-NGGWyCio/xjce6fPP7y3wwg7CheqllID6aJudDxbWhA=",
+ "dependencies": [
+ "bifunctors",
+ "const",
+ "control",
+ "either",
+ "functors",
+ "identity",
+ "maybe",
+ "newtype",
+ "orders",
+ "prelude",
+ "tuples"
+ ]
+ },
+ "foreign": {
+ "type": "registry",
+ "version": "7.0.0",
+ "integrity": "sha256-jiDRMVQfovGsbbA/FzbzYf6iWk9i2b5gNrIjz+gFfsI=",
+ "dependencies": [
+ "either",
+ "functions",
+ "integers",
+ "lists",
+ "maybe",
+ "prelude",
+ "strings",
+ "transformers",
+ "unsafe-coerce"
+ ]
+ },
+ "foreign-object": {
+ "type": "registry",
+ "version": "4.1.0",
+ "integrity": "sha256-x/Q7r80z/vmHRKvPwhYmHP/DnJciPtsyGfRTMzayKIU=",
+ "dependencies": [
+ "arrays",
+ "foldable-traversable",
+ "functions",
+ "gen",
+ "lists",
+ "maybe",
+ "prelude",
+ "st",
+ "tailrec",
+ "tuples",
+ "typelevel-prelude",
+ "unfoldable",
+ "unsafe-coerce"
+ ]
+ },
+ "fork": {
+ "type": "registry",
+ "version": "6.0.0",
+ "integrity": "sha256-0keBVeZZg1eCZEy/En1CohVW829gQUqKtqBNIRa93/A=",
+ "dependencies": [
+ "aff",
+ "prelude",
+ "transformers"
+ ]
+ },
+ "form-urlencoded": {
+ "type": "registry",
+ "version": "7.0.0",
+ "integrity": "sha256-+8F0S6thSLsC1GfQiTdTt6jyTMuhsgKLciBUzde9aFU=",
+ "dependencies": [
+ "foldable-traversable",
+ "js-uri",
+ "maybe",
+ "newtype",
+ "prelude",
+ "strings",
+ "tuples"
+ ]
+ },
+ "free": {
+ "type": "registry",
+ "version": "7.1.0",
+ "integrity": "sha256-tUBInfUpRQEw4u94GWcYW7EYdmSdDHSRn88a+C0eltU=",
+ "dependencies": [
+ "catenable-lists",
+ "control",
+ "distributive",
+ "either",
+ "exists",
+ "foldable-traversable",
+ "invariant",
+ "lazy",
+ "maybe",
+ "prelude",
+ "tailrec",
+ "transformers",
+ "tuples",
+ "unsafe-coerce"
+ ]
+ },
+ "freeap": {
+ "type": "registry",
+ "version": "7.0.0",
+ "integrity": "sha256-e7SiV9rg+rVARsU9cxA/02sps7ubogvVgYwj8nWosX4=",
+ "dependencies": [
+ "const",
+ "either",
+ "gen",
+ "lists",
+ "newtype",
+ "nonempty",
+ "prelude",
+ "tailrec",
+ "tuples",
+ "unsafe-coerce"
+ ]
+ },
+ "functions": {
+ "type": "registry",
+ "version": "6.0.0",
+ "integrity": "sha256-0jgtOr+SFO4UScKc40oA/9Vdcf2rLn3h3vZlY0KfqVo=",
+ "dependencies": [
+ "prelude"
+ ]
+ },
+ "functors": {
+ "type": "registry",
+ "version": "5.0.0",
+ "integrity": "sha256-W1o/4wcp6S3chodYL8uixlXK2mfr1L+SzM1R+whqUco=",
+ "dependencies": [
+ "bifunctors",
+ "const",
+ "contravariant",
+ "control",
+ "distributive",
+ "either",
+ "invariant",
+ "maybe",
+ "newtype",
+ "prelude",
+ "profunctor",
+ "tuples",
+ "unsafe-coerce"
+ ]
+ },
+ "gen": {
+ "type": "registry",
+ "version": "4.0.0",
+ "integrity": "sha256-kyOateeOtQa07vfTMYeSuCKdelgKE1R65fwKkZ10wsY=",
+ "dependencies": [
+ "either",
+ "foldable-traversable",
+ "identity",
+ "maybe",
+ "newtype",
+ "nonempty",
+ "prelude",
+ "tailrec",
+ "tuples",
+ "unfoldable"
+ ]
+ },
+ "halogen": {
+ "type": "registry",
+ "version": "7.0.0",
+ "integrity": "sha256-YrUm27QEiE1DyGaGJ5MNLVg4H/P3gnX/NVv1gRtWjB4=",
+ "dependencies": [
+ "aff",
+ "bifunctors",
+ "console",
+ "control",
+ "dom-indexed",
+ "effect",
+ "either",
+ "exceptions",
+ "foldable-traversable",
+ "foreign",
+ "fork",
+ "free",
+ "freeap",
+ "functions",
+ "halogen-subscriptions",
+ "halogen-vdom",
+ "lazy",
+ "lists",
+ "maybe",
+ "media-types",
+ "newtype",
+ "ordered-collections",
+ "parallel",
+ "prelude",
+ "profunctor",
+ "refs",
+ "strings",
+ "tailrec",
+ "transformers",
+ "tuples",
+ "unfoldable",
+ "unsafe-coerce",
+ "unsafe-reference",
+ "web-clipboard",
+ "web-dom",
+ "web-events",
+ "web-file",
+ "web-html",
+ "web-touchevents",
+ "web-uievents"
+ ]
+ },
+ "halogen-subscriptions": {
+ "type": "registry",
+ "version": "2.0.0",
+ "integrity": "sha256-1eBtVZENgGtKuOY9H0iuYD3dO1CSqmOIyhYe4OhypOU=",
+ "dependencies": [
+ "arrays",
+ "contravariant",
+ "control",
+ "effect",
+ "foldable-traversable",
+ "maybe",
+ "prelude",
+ "refs",
+ "safe-coerce",
+ "unsafe-reference"
+ ]
+ },
+ "halogen-vdom": {
+ "type": "registry",
+ "version": "8.0.0",
+ "integrity": "sha256-jk6aj/tH630skFVk6mB5Q+0g2zb897KX72T63fJhJ2Y=",
+ "dependencies": [
+ "arrays",
+ "bifunctors",
+ "effect",
+ "foreign",
+ "foreign-object",
+ "functions",
+ "maybe",
+ "newtype",
+ "nullable",
+ "prelude",
+ "refs",
+ "tuples",
+ "unsafe-coerce",
+ "web-dom",
+ "web-events"
+ ]
+ },
+ "http-methods": {
+ "type": "registry",
+ "version": "6.0.0",
+ "integrity": "sha256-S0UjrJSgwJxEsFHM/pX77dM8V4HfvvDqdHPckQrtxMs=",
+ "dependencies": [
+ "either",
+ "prelude",
+ "strings"
+ ]
+ },
+ "identity": {
+ "type": "registry",
+ "version": "6.0.0",
+ "integrity": "sha256-RY/iXPpxpqvKHYfJeFVNI1J06kohB9rbtWYuha8t0LE=",
+ "dependencies": [
+ "control",
+ "invariant",
+ "newtype",
+ "prelude"
+ ]
+ },
+ "integers": {
+ "type": "registry",
+ "version": "6.0.0",
+ "integrity": "sha256-Y8yozC1uHRLcruobEyv5/bMKzE1BoKJ/olHv6o+bgm8=",
+ "dependencies": [
+ "maybe",
+ "numbers",
+ "prelude"
+ ]
+ },
+ "invariant": {
+ "type": "registry",
+ "version": "6.0.0",
+ "integrity": "sha256-HyoT1I5qIHRq8RSZji6zZDvqnpVjLw748utrKwZtqsw=",
+ "dependencies": [
+ "control",
+ "prelude"
+ ]
+ },
+ "js-date": {
+ "type": "registry",
+ "version": "8.0.0",
+ "integrity": "sha256-DNQrp4xYc8o80jtxo0aOkUeXGi6h0Xidkk2ZQ8Y4l5Y=",
+ "dependencies": [
+ "datetime",
+ "effect",
+ "enums",
+ "foreign",
+ "functions",
+ "integers",
+ "maybe",
+ "prelude"
+ ]
+ },
+ "js-fetch": {
+ "type": "registry",
+ "version": "0.2.1",
+ "integrity": "sha256-jsMrMh8kIUW5bHbHl7hy8ev/iMru1MzfDa+hoAxo14Q=",
+ "dependencies": [
+ "arraybuffer-types",
+ "arrays",
+ "effect",
+ "foldable-traversable",
+ "foreign",
+ "foreign-object",
+ "functions",
+ "http-methods",
+ "js-promise",
+ "maybe",
+ "newtype",
+ "prelude",
+ "record",
+ "tuples",
+ "typelevel-prelude",
+ "unfoldable",
+ "web-file",
+ "web-streams"
+ ]
+ },
+ "js-promise": {
+ "type": "registry",
+ "version": "1.0.0",
+ "integrity": "sha256-u+6+ofIwT5XhT2fbMu4dShwkyWGfPRt9642Cnzw/Wvk=",
+ "dependencies": [
+ "effect",
+ "exceptions",
+ "foldable-traversable",
+ "functions",
+ "maybe",
+ "newtype",
+ "prelude"
+ ]
+ },
+ "js-promise-aff": {
+ "type": "registry",
+ "version": "1.0.0",
+ "integrity": "sha256-gEKAdNnTWjXS+tTp/AhYl/ezloKfuoQFcBKrdOx2USo=",
+ "dependencies": [
+ "aff",
+ "control",
+ "effect",
+ "either",
+ "exceptions",
+ "foreign",
+ "js-promise",
+ "maybe",
+ "prelude",
+ "transformers"
+ ]
+ },
+ "js-uri": {
+ "type": "registry",
+ "version": "3.1.0",
+ "integrity": "sha256-7waHOctAfqhhw540FNJ7C/YsDMQW9TmA+SpHhduH6Uw=",
+ "dependencies": [
+ "functions",
+ "maybe",
+ "prelude"
+ ]
+ },
+ "lazy": {
+ "type": "registry",
+ "version": "6.0.0",
+ "integrity": "sha256-MHRC+1Pc+AjortBp4b/e1e6KpEpsDwaJBNBB7TOL+1I=",
+ "dependencies": [
+ "control",
+ "foldable-traversable",
+ "invariant",
+ "prelude"
+ ]
+ },
+ "lists": {
+ "type": "registry",
+ "version": "7.0.0",
+ "integrity": "sha256-/mGd7wLoU+wsTcqii7SSUByxsvwvjnu2PjwAD47yYb4=",
+ "dependencies": [
+ "bifunctors",
+ "control",
+ "foldable-traversable",
+ "lazy",
+ "maybe",
+ "newtype",
+ "nonempty",
+ "partial",
+ "prelude",
+ "tailrec",
+ "tuples",
+ "unfoldable"
+ ]
+ },
+ "maybe": {
+ "type": "registry",
+ "version": "6.0.0",
+ "integrity": "sha256-8X6XEgtZ5lqfkEuc1hWOofnL6mwhk8gSJTxwvbR/SbY=",
+ "dependencies": [
+ "control",
+ "invariant",
+ "newtype",
+ "prelude"
+ ]
+ },
+ "media-types": {
+ "type": "registry",
+ "version": "6.0.0",
+ "integrity": "sha256-3Wyq+vfqJFTmy1k6DGVnXStxtZxgK631wusDPzlFVxg=",
+ "dependencies": [
+ "newtype",
+ "prelude"
+ ]
+ },
+ "newtype": {
+ "type": "registry",
+ "version": "5.0.0",
+ "integrity": "sha256-dt6w0cty4OS+Pjt9fsh3iU6ottqSU83mdAZkaEEvo6c=",
+ "dependencies": [
+ "prelude",
+ "safe-coerce"
+ ]
+ },
+ "node-buffer": {
+ "type": "registry",
+ "version": "9.0.0",
+ "integrity": "sha256-RdQjilmEHqsT4fwpCr8Mavw8KTafYrS3ZM6zeAAVAl8=",
+ "dependencies": [
+ "arraybuffer-types",
+ "effect",
+ "functions",
+ "maybe",
+ "nullable",
+ "partial",
+ "prelude",
+ "st",
+ "unsafe-coerce"
+ ]
+ },
+ "node-event-emitter": {
+ "type": "registry",
+ "version": "3.0.0",
+ "integrity": "sha256-wodx71NxJBPXOaawFn01V1ByYB2vT+I3RuV2giOVKMg=",
+ "dependencies": [
+ "effect",
+ "either",
+ "functions",
+ "maybe",
+ "nullable",
+ "prelude",
+ "unsafe-coerce"
+ ]
+ },
+ "node-fs": {
+ "type": "registry",
+ "version": "9.2.0",
+ "integrity": "sha256-fxioTSm9y47WuYs9F/7nxCh/RVDCl3Kr3Hg779J4SJA=",
+ "dependencies": [
+ "aff",
+ "datetime",
+ "effect",
+ "either",
+ "enums",
+ "exceptions",
+ "functions",
+ "integers",
+ "js-date",
+ "maybe",
+ "node-buffer",
+ "node-path",
+ "node-streams",
+ "nullable",
+ "partial",
+ "prelude",
+ "strings"
+ ]
+ },
+ "node-http": {
+ "type": "registry",
+ "version": "9.1.0",
+ "integrity": "sha256-V6EFML/uqvBEUlQsph0VPuLQCtZdn51HJTa6yxh7YTk=",
+ "dependencies": [
+ "arrays",
+ "datetime",
+ "effect",
+ "exceptions",
+ "foreign",
+ "foreign-object",
+ "maybe",
+ "node-buffer",
+ "node-event-emitter",
+ "node-net",
+ "node-streams",
+ "node-tls",
+ "node-url",
+ "nullable",
+ "prelude",
+ "unsafe-coerce"
+ ]
+ },
+ "node-net": {
+ "type": "registry",
+ "version": "5.1.0",
+ "integrity": "sha256-e2kxHXolOzYD/OkZekYlVPwCl/b4WqyLOodA6QX7FQI=",
+ "dependencies": [
+ "datetime",
+ "effect",
+ "exceptions",
+ "maybe",
+ "node-event-emitter",
+ "node-fs",
+ "node-streams",
+ "nullable",
+ "partial",
+ "prelude",
+ "unsafe-coerce"
+ ]
+ },
+ "node-path": {
+ "type": "registry",
+ "version": "5.0.1",
+ "integrity": "sha256-j7n/SmpVz0FOMJl4XZop3ofJ+MeIPkMNqUEUKHEL6Us=",
+ "dependencies": [
+ "effect"
+ ]
+ },
+ "node-process": {
+ "type": "registry",
+ "version": "11.2.0",
+ "integrity": "sha256-onzKeNmaeYfN3HSDPqSFTrrP2Yl9tnVsfKKhnf2plxM=",
+ "dependencies": [
+ "effect",
+ "exceptions",
+ "foreign",
+ "foreign-object",
+ "maybe",
+ "node-event-emitter",
+ "node-streams",
+ "nullable",
+ "posix-types",
+ "prelude",
+ "strings"
+ ]
+ },
+ "node-streams": {
+ "type": "registry",
+ "version": "9.0.1",
+ "integrity": "sha256-yuVGm/R/tBr4Nphi+a1a984Z0fkmN9Q5wWtSmbmJTpA=",
+ "dependencies": [
+ "aff",
+ "arrays",
+ "effect",
+ "either",
+ "exceptions",
+ "maybe",
+ "node-buffer",
+ "node-event-emitter",
+ "nullable",
+ "prelude",
+ "refs",
+ "st",
+ "tailrec",
+ "unsafe-coerce"
+ ]
+ },
+ "node-tls": {
+ "type": "registry",
+ "version": "0.3.1",
+ "integrity": "sha256-TRTFNDfY65UUN5dZO+00qwNc4Ab7cpj2mi7B0VIKq3w=",
+ "dependencies": [
+ "effect",
+ "either",
+ "exceptions",
+ "foreign",
+ "maybe",
+ "node-buffer",
+ "node-event-emitter",
+ "node-net",
+ "nullable",
+ "partial",
+ "prelude",
+ "unsafe-coerce"
+ ]
+ },
+ "node-url": {
+ "type": "registry",
+ "version": "7.0.1",
+ "integrity": "sha256-4h+vxO0RMrLN85hAQnoOjN6qD0jRIODhQcBIJXkat/4=",
+ "dependencies": [
+ "effect",
+ "foreign",
+ "functions",
+ "maybe",
+ "nullable",
+ "prelude",
+ "tuples"
+ ]
+ },
+ "nonempty": {
+ "type": "registry",
+ "version": "7.0.0",
+ "integrity": "sha256-Ctkq8/+KiGqCE53Y/LvibA/coy7ev9HtRZU+GYS7yfQ=",
+ "dependencies": [
+ "control",
+ "foldable-traversable",
+ "maybe",
+ "prelude",
+ "tuples",
+ "unfoldable"
+ ]
+ },
+ "now": {
+ "type": "registry",
+ "version": "6.0.0",
+ "integrity": "sha256-mTyG4s8tps3XIf0jjV/lME2/QlwMzrQly2wqfSSUM3o=",
+ "dependencies": [
+ "datetime",
+ "effect",
+ "prelude"
+ ]
+ },
+ "nullable": {
+ "type": "registry",
+ "version": "6.0.0",
+ "integrity": "sha256-CDAZk+L9Sc0GCFTkE8n+qdp4Ys8avvnkU+m2hVALt7M=",
+ "dependencies": [
+ "functions",
+ "maybe",
+ "prelude"
+ ]
+ },
+ "numbers": {
+ "type": "registry",
+ "version": "9.0.1",
+ "integrity": "sha256-ZydYckgwRAnRaFC7pu6fADhzdwX+UqHZrfJyet64zls=",
+ "dependencies": [
+ "functions",
+ "maybe",
+ "prelude"
+ ]
+ },
+ "ordered-collections": {
+ "type": "registry",
+ "version": "3.2.0",
+ "integrity": "sha256-NaEXc2cz/7lJfxPWEja4XlZbGxuRqJwFKQJjsJf51kQ=",
+ "dependencies": [
+ "arrays",
+ "control",
+ "foldable-traversable",
+ "functions",
+ "gen",
+ "lists",
+ "maybe",
+ "newtype",
+ "partial",
+ "prelude",
+ "safe-coerce",
+ "tailrec",
+ "tuples",
+ "unfoldable"
+ ]
+ },
+ "orders": {
+ "type": "registry",
+ "version": "6.0.0",
+ "integrity": "sha256-nSMxZK7wxyDULkjZqvMyB71mVo6b0AyLLHosDVbv1XM=",
+ "dependencies": [
+ "newtype",
+ "prelude"
+ ]
+ },
+ "parallel": {
+ "type": "registry",
+ "version": "7.0.0",
+ "integrity": "sha256-bAGpVhrFz2IyHkA4MR7+tE/19FZ7dzYBdYPgK5svkqw=",
+ "dependencies": [
+ "control",
+ "effect",
+ "either",
+ "foldable-traversable",
+ "functors",
+ "maybe",
+ "newtype",
+ "prelude",
+ "profunctor",
+ "refs",
+ "transformers"
+ ]
+ },
+ "partial": {
+ "type": "registry",
+ "version": "4.0.0",
+ "integrity": "sha256-mdrlJBAgFMh79hNRW39uv7c+BwnnaOrAMmdh7+VhEhs=",
+ "dependencies": []
+ },
+ "posix-types": {
+ "type": "registry",
+ "version": "6.0.0",
+ "integrity": "sha256-Je5jG439F/SFO/IvVevZnWVxDIonrehlKuJkefP2lHE=",
+ "dependencies": [
+ "maybe",
+ "newtype",
+ "prelude"
+ ]
+ },
+ "prelude": {
+ "type": "registry",
+ "version": "6.0.2",
+ "integrity": "sha256-IqykrDRquGUD/LRSad7y75hZv3bUsGQ4knc8tdez8Qw=",
+ "dependencies": []
+ },
+ "profunctor": {
+ "type": "registry",
+ "version": "6.0.1",
+ "integrity": "sha256-Ow0mJC+PIIRq/a3mzCWY5vxuTgR/e+Ee68+mWeSRnWY=",
+ "dependencies": [
+ "control",
+ "distributive",
+ "either",
+ "exists",
+ "invariant",
+ "newtype",
+ "prelude",
+ "tuples"
+ ]
+ },
+ "profunctor-lenses": {
+ "type": "registry",
+ "version": "8.0.0",
+ "integrity": "sha256-mVIYR3kEMHyO2m+VicJeZeVyDJ4IgvkMzDll3JdnRN4=",
+ "dependencies": [
+ "arrays",
+ "bifunctors",
+ "const",
+ "control",
+ "distributive",
+ "either",
+ "foldable-traversable",
+ "foreign-object",
+ "functors",
+ "identity",
+ "lists",
+ "maybe",
+ "newtype",
+ "ordered-collections",
+ "partial",
+ "prelude",
+ "profunctor",
+ "record",
+ "safe-coerce",
+ "transformers",
+ "tuples"
+ ]
+ },
+ "record": {
+ "type": "registry",
+ "version": "4.0.0",
+ "integrity": "sha256-WQ8fqp4X1wTA70J4qUcPW4Z7DCwx6e5KJnT/EuTdWSc=",
+ "dependencies": [
+ "functions",
+ "prelude",
+ "unsafe-coerce"
+ ]
+ },
+ "refs": {
+ "type": "registry",
+ "version": "6.0.0",
+ "integrity": "sha256-KKD5G9dR2SauuDGULUJsxjqDOEi1Jqm+2Sq5U2mY8YU=",
+ "dependencies": [
+ "effect",
+ "prelude"
+ ]
+ },
+ "safe-coerce": {
+ "type": "registry",
+ "version": "2.0.0",
+ "integrity": "sha256-EJrxtKt5xC7TYjBZrSBO/J7Aw3DmtilYjt4olpAXWVc=",
+ "dependencies": [
+ "unsafe-coerce"
+ ]
+ },
+ "st": {
+ "type": "registry",
+ "version": "6.2.0",
+ "integrity": "sha256-WI4MEkkwUd4pnwZQ3G8VKWgX5t1RSNJludm5e9boaRo=",
+ "dependencies": [
+ "effect",
+ "partial",
+ "prelude",
+ "tailrec",
+ "unsafe-coerce"
+ ]
+ },
+ "strings": {
+ "type": "registry",
+ "version": "6.0.1",
+ "integrity": "sha256-FNto2hoNW5Da6W6crIk77YXMagBvOGHZE1kO7eZ1vLM=",
+ "dependencies": [
+ "arrays",
+ "control",
+ "either",
+ "enums",
+ "foldable-traversable",
+ "gen",
+ "integers",
+ "maybe",
+ "newtype",
+ "nonempty",
+ "partial",
+ "prelude",
+ "tailrec",
+ "tuples",
+ "unfoldable",
+ "unsafe-coerce"
+ ]
+ },
+ "tailrec": {
+ "type": "registry",
+ "version": "6.1.0",
+ "integrity": "sha256-xrorCfP0xV1wubs27idQCHQqdvSUuqmmL/am7Ji1wVU=",
+ "dependencies": [
+ "bifunctors",
+ "effect",
+ "either",
+ "identity",
+ "maybe",
+ "partial",
+ "prelude",
+ "refs"
+ ]
+ },
+ "transformers": {
+ "type": "registry",
+ "version": "6.1.0",
+ "integrity": "sha256-QJmgNT/y7ljPHSsXaW4zxF/982BFiQ90KNL1g/NtEOQ=",
+ "dependencies": [
+ "control",
+ "distributive",
+ "effect",
+ "either",
+ "exceptions",
+ "foldable-traversable",
+ "identity",
+ "lazy",
+ "maybe",
+ "newtype",
+ "prelude",
+ "st",
+ "tailrec",
+ "tuples",
+ "unfoldable"
+ ]
+ },
+ "tuples": {
+ "type": "registry",
+ "version": "7.0.0",
+ "integrity": "sha256-BKy7EfK8S1KeTCwyWqyW1wwcpSAQuh3IsczSVgS6fzY=",
+ "dependencies": [
+ "control",
+ "invariant",
+ "prelude"
+ ]
+ },
+ "type-equality": {
+ "type": "registry",
+ "version": "4.0.1",
+ "integrity": "sha256-BBqYOSnAKmayRvR5mtZoCf4SlYs2ipatD/+5nQx73aw=",
+ "dependencies": []
+ },
+ "typelevel-prelude": {
+ "type": "registry",
+ "version": "7.0.0",
+ "integrity": "sha256-+pHi14/40mTj/+lmCWqL0T7iXIXD1+NSg/KItyyoB0A=",
+ "dependencies": [
+ "prelude",
+ "type-equality"
+ ]
+ },
+ "unfoldable": {
+ "type": "registry",
+ "version": "6.0.0",
+ "integrity": "sha256-BK/6By1sKp/ztk+9CF3q43SV0QDTLVdsGqFerKHSmbg=",
+ "dependencies": [
+ "foldable-traversable",
+ "maybe",
+ "partial",
+ "prelude",
+ "tuples"
+ ]
+ },
+ "unsafe-coerce": {
+ "type": "registry",
+ "version": "6.0.0",
+ "integrity": "sha256-0L1QsaY20OILjfU5TV72d3U/tSjhmL9hJ32WMp857Lk=",
+ "dependencies": []
+ },
+ "unsafe-reference": {
+ "type": "registry",
+ "version": "5.0.0",
+ "integrity": "sha256-ttSJTQUnK8AK2eGOsfRxLUJEDPL6pnmExjbiOqfwC6I=",
+ "dependencies": [
+ "prelude"
+ ]
+ },
+ "web-clipboard": {
+ "type": "registry",
+ "version": "6.0.0",
+ "integrity": "sha256-3CV0txwKddLoj8qBrRjUWbiX0M8+rnOpi1iApr5PEb0=",
+ "dependencies": [
+ "effect",
+ "functions",
+ "js-promise",
+ "maybe",
+ "nullable",
+ "prelude",
+ "unsafe-coerce",
+ "web-events",
+ "web-html"
+ ]
+ },
+ "web-dom": {
+ "type": "registry",
+ "version": "6.0.0",
+ "integrity": "sha256-rVeqkxChkjqisDvOujy9BMEgGqJkwX3t6g21bXfiG5o=",
+ "dependencies": [
+ "effect",
+ "enums",
+ "maybe",
+ "newtype",
+ "nullable",
+ "prelude",
+ "unsafe-coerce",
+ "web-events"
+ ]
+ },
+ "web-events": {
+ "type": "registry",
+ "version": "4.0.0",
+ "integrity": "sha256-wZNeldG0OU+8qGycUJ9e6vxf1/GC4Ep4OlRMhvjs9LA=",
+ "dependencies": [
+ "datetime",
+ "effect",
+ "enums",
+ "foreign",
+ "functions",
+ "maybe",
+ "newtype",
+ "nullable",
+ "prelude",
+ "unsafe-coerce"
+ ]
+ },
+ "web-file": {
+ "type": "registry",
+ "version": "4.0.0",
+ "integrity": "sha256-txsdkeWBKU/+5v9m5c6kysgJQSDyV1r7MHaeT4STpjQ=",
+ "dependencies": [
+ "datetime",
+ "effect",
+ "enums",
+ "foreign",
+ "integers",
+ "maybe",
+ "media-types",
+ "nullable",
+ "numbers",
+ "partial",
+ "prelude",
+ "tuples",
+ "unfoldable",
+ "unsafe-coerce",
+ "web-events"
+ ]
+ },
+ "web-html": {
+ "type": "registry",
+ "version": "4.1.1",
+ "integrity": "sha256-yOuZJGUxFrivHKpo/jKuKzFZaq6PPPE64ftSysXFssk=",
+ "dependencies": [
+ "effect",
+ "enums",
+ "foreign",
+ "functions",
+ "js-date",
+ "maybe",
+ "media-types",
+ "newtype",
+ "nullable",
+ "prelude",
+ "unsafe-coerce",
+ "web-dom",
+ "web-events",
+ "web-file",
+ "web-storage"
+ ]
+ },
+ "web-pointerevents": {
+ "type": "registry",
+ "version": "2.0.0",
+ "integrity": "sha256-mQbm1V36u3It2WdYMAXtzLBJl3uRAhN4BjeZTw3LWN8=",
+ "dependencies": [
+ "effect",
+ "maybe",
+ "prelude",
+ "unsafe-coerce",
+ "web-dom",
+ "web-events",
+ "web-html",
+ "web-uievents"
+ ]
+ },
+ "web-storage": {
+ "type": "registry",
+ "version": "5.0.0",
+ "integrity": "sha256-YJfos55oeKtBStNfoWfeflSy8j+4IDym5eaZtIY7YM4=",
+ "dependencies": [
+ "effect",
+ "maybe",
+ "nullable",
+ "prelude",
+ "unsafe-coerce",
+ "web-events"
+ ]
+ },
+ "web-streams": {
+ "type": "registry",
+ "version": "4.0.0",
+ "integrity": "sha256-iKQbHh/3wNOWyGTZmWJZ1Yuri9J/wQIJKXcxrELaHVo=",
+ "dependencies": [
+ "arraybuffer-types",
+ "effect",
+ "exceptions",
+ "js-promise",
+ "maybe",
+ "nullable",
+ "prelude",
+ "tuples"
+ ]
+ },
+ "web-touchevents": {
+ "type": "registry",
+ "version": "4.0.0",
+ "integrity": "sha256-H02c53BJlHDlAgogj/x8bM7PagJjhMMrMPYy9I7Rf0s=",
+ "dependencies": [
+ "functions",
+ "maybe",
+ "nullable",
+ "prelude",
+ "unsafe-coerce",
+ "web-events",
+ "web-uievents"
+ ]
+ },
+ "web-uievents": {
+ "type": "registry",
+ "version": "5.0.0",
+ "integrity": "sha256-yh9uVLIGu8wE1+jwLehtfsRfR1eyiShhIMMhBPqKybE=",
+ "dependencies": [
+ "effect",
+ "enums",
+ "maybe",
+ "nullable",
+ "prelude",
+ "unsafe-coerce",
+ "web-events",
+ "web-html"
+ ]
+ },
+ "web-xhr": {
+ "type": "registry",
+ "version": "5.0.1",
+ "integrity": "sha256-gxQgeGwvF/NyGdw0FJ9IzXI3gqIMNb8oQO8X+p5jDyw=",
+ "dependencies": [
+ "arraybuffer-types",
+ "datetime",
+ "effect",
+ "either",
+ "enums",
+ "foreign",
+ "http-methods",
+ "maybe",
+ "media-types",
+ "newtype",
+ "nullable",
+ "prelude",
+ "unsafe-coerce",
+ "web-dom",
+ "web-events",
+ "web-file",
+ "web-html"
+ ]
+ }
+ }
+}
diff --git a/spago.yaml b/spago.yaml
@@ -0,0 +1,44 @@
+package:
+ name: scrobbler-purescript
+ dependencies:
+ - aff
+ - affjax
+ - affjax-web
+ - argonaut
+ - argonaut-core
+ - arrays
+ - console
+ - datetime
+ - effect
+ - either
+ - exceptions
+ - fetch
+ - fetch-argonaut
+ - foreign-object
+ - halogen
+ - http-methods
+ - integers
+ - js-uri
+ - maybe
+ - node-buffer
+ - node-event-emitter
+ - node-fs
+ - node-http
+ - node-net
+ - node-process
+ - node-streams
+ - node-url
+ - now
+ - ordered-collections
+ - prelude
+ - strings
+ - tailrec
+ - unsafe-coerce
+ - web-dom
+ test:
+ main: Test.Main
+ dependencies: []
+workspace:
+ packageSet:
+ registry: 75.9.0
+ extraPackages: {}
diff --git a/src/Client.purs b/src/Client.purs
@@ -0,0 +1,213 @@
+module Client where
+
+import Prelude
+
+import Affjax.Web as AX
+import Affjax.ResponseFormat as ResponseFormat
+import Data.Argonaut (decodeJson)
+import Data.Array (mapWithIndex)
+import Data.DateTime.Instant (unInstant)
+import Data.Either (Either(..))
+import Data.Int (floor)
+import Data.Maybe (Maybe(..), fromMaybe)
+import Effect (Effect)
+import Effect.Console as Console
+import Effect.Aff (Aff, delay)
+import Effect.Aff.Class (class MonadAff)
+import Effect.Class (liftEffect)
+import Effect.Now (now)
+import Control.Monad.Rec.Class (forever)
+import Halogen as H
+import Halogen.Aff as HA
+import Halogen.HTML as HH
+import Halogen.HTML.Events as HE
+import Halogen.HTML.Properties as HP
+import Halogen.VDom.Driver (runUI)
+import JSURI (encodeURIComponent)
+import Web.DOM.ParentNode (QuerySelector(..))
+import Types (Listen(..), ListenBrainzResponse(..), TrackMetadata(..), Payload(..), MbidMapping(..))
+import Data.Time.Duration (Milliseconds(..))
+
+import Data.Set (Set)
+import Data.Set as Set
+
+type State =
+ { listens :: Array Listen
+ , lastCheck :: Maybe String
+ , error :: Maybe String
+ , loading :: Boolean
+ , currentTime :: Maybe Milliseconds
+ , failedCovers :: Set String
+ }
+
+data Action
+ = Initialize
+ | Refresh
+ | ReceiveResponse (Either String (Array Listen))
+ | ImageError String
+
+component :: forall query input output m. MonadAff m => H.Component query input output m
+component =
+ H.mkComponent
+ { initialState
+ , render
+ , eval: H.mkEval $ H.defaultEval
+ { handleAction = handleAction
+ , initialize = Just Initialize
+ }
+ }
+ where
+ initialState _ =
+ { listens: []
+ , lastCheck: Nothing
+ , error: Nothing
+ , loading: true
+ , currentTime: Nothing
+ , failedCovers: Set.empty
+ }
+
+ render state =
+ HH.div
+ [ HP.class_ (H.ClassName "container") ]
+ [ HH.h1_ [ HH.text "Recent Tracks" ]
+ , renderContent state
+ , HH.p
+ [ HP.id "last-updated"
+ , HP.class_ (H.ClassName "small")
+ ]
+ [ HH.text $ fromMaybe "" state.lastCheck ]
+ ]
+
+ renderContent state
+ | state.loading && state.listens == [] =
+ HH.ul_ [ HH.li [ HP.class_ (H.ClassName "loading") ] [ HH.text "Loading recent tracks..." ] ]
+ | Just err <- state.error =
+ HH.ul_ [ HH.li [ HP.class_ (H.ClassName "error") ] [ HH.text err ] ]
+ | otherwise =
+ HH.ul [ HP.id "tracks-container" ]
+ (mapWithIndex (renderListen state.currentTime state.failedCovers) state.listens)
+
+ renderListen currentTime failedCovers _ (Listen { trackMetadata: TrackMetadata track, listenedAt }) =
+ let
+ release = fromMaybe "" track.releaseName
+ artist = fromMaybe "" track.artistName
+
+ -- Priority:
+ -- 1. CAA (CAA MBID)
+ -- 2. CAA (Release MBID)
+ -- 3. Last.fm proxy
+ -- 4. Discogs proxy
+
+ mbid = case track.mbidMapping of
+ Just (MbidMapping { caaReleaseMbid: Just m }) -> Just m
+ Just (MbidMapping { releaseMbid: Just m }) -> Just m
+ _ -> Nothing
+
+ lastfmId = "lastfm-" <> artist <> "-" <> release
+ discogsId = "discogs-" <> artist <> "-" <> release
+
+ coverInfo = case mbid of
+ Just m ->
+ if Set.member m failedCovers then
+ -- Fallback 1: Last.fm
+ if Set.member lastfmId failedCovers then
+ -- Fallback 2: Discogs
+ if Set.member discogsId failedCovers then Nothing
+ else Just { id: discogsId, url: "/discogs?artist=" <> (fromMaybe "" $ encodeURIComponent artist) <> "&release=" <> (fromMaybe "" $ encodeURIComponent release) }
+ else Just { id: lastfmId, url: "/lastfm?artist=" <> (fromMaybe "" $ encodeURIComponent artist) <> "&release=" <> (fromMaybe "" $ encodeURIComponent release) }
+ else Just { id: m, url: "https://coverartarchive.org/release/" <> m <> "/front-250" }
+ Nothing ->
+ -- No MBID, try Last.fm then Discogs
+ if Set.member lastfmId failedCovers then
+ if Set.member discogsId failedCovers then Nothing
+ else Just { id: discogsId, url: "/discogs?artist=" <> (fromMaybe "" $ encodeURIComponent artist) <> "&release=" <> (fromMaybe "" $ encodeURIComponent release) }
+ else Just { id: lastfmId, url: "/lastfm?artist=" <> (fromMaybe "" $ encodeURIComponent artist) <> "&release=" <> (fromMaybe "" $ encodeURIComponent release) }
+ in
+ HH.li
+ [ HP.class_ (H.ClassName "success") ]
+ [ HH.div
+ [ HP.class_ (H.ClassName "track-info") ]
+ [ HH.div
+ [ HP.class_ (H.ClassName "track-name") ]
+ [ HH.text $ fromMaybe "Unknown Track" track.trackName ]
+ , HH.div
+ [ HP.class_ (H.ClassName "track-artist") ]
+ [ HH.text artist ]
+ , HH.div
+ [ HP.class_ (H.ClassName "track-time") ]
+ [ let
+ query = fromMaybe "" $ encodeURIComponent (artist <> " " <> release)
+ in
+ HH.span_
+ [ HH.a
+ [ HP.href $ "https://www.discogs.com/search/?q=" <> query <> "&type=release"
+ , HP.target "_blank"
+ , HP.class_ (H.ClassName "album-link")
+ ]
+ [ HH.text release ]
+ , HH.text $ " • " <> (fromMaybe "unknown time" $ formatTimeAgo currentTime listenedAt)
+ ]
+ ]
+ ]
+ , case coverInfo of
+ Just { id, url } ->
+ HH.img
+ [ HP.class_ (H.ClassName "track-cover")
+ , HP.src url
+ , HP.alt release
+ , HE.onError \_ -> ImageError id
+ ]
+ Nothing -> HH.text ""
+ ]
+
+ handleAction = case _ of
+ Initialize -> do
+ void $ H.fork $ forever (H.liftAff (delay (Milliseconds 30000.0)) *> handleAction Refresh)
+ handleAction Refresh
+ Refresh -> do
+ H.modify_ _ { loading = true, error = Nothing }
+ response <- H.liftAff fetchListens
+ handleAction (ReceiveResponse response)
+ ReceiveResponse result -> do
+ nowInstant <- liftEffect now
+ let nowMs = unInstant nowInstant
+ let nowStr = "Last check: " <> show nowInstant
+ case result of
+ Left err -> H.modify_ _ { loading = false, error = Just err, lastCheck = Just nowStr, currentTime = Just nowMs }
+ Right listens -> H.modify_ _ { loading = false, listens = listens, lastCheck = Just nowStr, currentTime = Just nowMs }
+ ImageError mbid -> do
+ liftEffect $ Console.log $ "Image load failed for: " <> mbid
+ H.modify_ \state -> state { failedCovers = Set.insert mbid state.failedCovers }
+
+ fetchListens :: Aff (Either String (Array Listen))
+ fetchListens = do
+ res <- AX.get ResponseFormat.json "/proxy"
+ case res of
+ Left err -> pure $ Left $ "Network error: " <> AX.printError err
+ Right response ->
+ case decodeJson response.body of
+ Left err -> pure $ Left $ "JSON decode error: " <> show err
+ Right (ListenBrainzResponse { payload: Payload { listens } }) -> pure $ Right listens
+
+ formatTimeAgo :: Maybe Milliseconds -> Maybe Int -> Maybe String
+ formatTimeAgo Nothing _ = Nothing
+ formatTimeAgo _ Nothing = Nothing
+ formatTimeAgo (Just (Milliseconds nowMs)) (Just timestamp) =
+ let
+ nowSecs = floor (nowMs / 1000.0)
+ diff = nowSecs - timestamp
+ in Just $
+ if diff < 60 then "just now"
+ else if diff < 3600 then
+ let mins = diff / 60 in show mins <> " minute" <> (if mins > 1 then "s" else "") <> " ago"
+ else if diff < 86400 then
+ let hours = diff / 3600 in show hours <> " hour" <> (if hours > 1 then "s" else "") <> " ago"
+ else
+ let days = diff / 86400 in show days <> " day" <> (if days > 1 then "s" else "") <> " ago"
+
+main :: Effect Unit
+main = HA.runHalogenAff do
+ maybeApp <- HA.selectElement (QuerySelector "#app")
+ case maybeApp of
+ Nothing -> HA.awaitBody >>= runUI component unit
+ Just app -> runUI component unit app
diff --git a/src/Main.purs b/src/Main.purs
@@ -15,7 +15,7 @@ import Node.HTTP.IncomingMessage as IM
import Node.HTTP.Types (ServerResponse, IncomingMessage, IMServer)
import Node.HTTP.ServerResponse (setStatusCode, toOutgoingMessage)
import Node.HTTP.OutgoingMessage (setHeader, toWriteable)
-import Node.Stream (end, writeString)
+import Node.Stream (end, writeString, pipe)
import Node.Stream.Aff (readableToStringUtf8)
import Node.Encoding (Encoding(UTF8))
import Node.Net.Server (listenTcp, listeningH)
@@ -23,16 +23,23 @@ import Data.Either (Either(..))
import Effect.Exception as Exception
import Effect.Aff (Aff, launchAff_, makeAff, nonCanceler, try)
import Unsafe.Coerce (unsafeCoerce)
+import Node.FS.Aff as FSA
+import Node.Process (getEnv)
+import Fetch (fetch, Method(GET))
+import Fetch.Argonaut.Json (fromJson)
+import Data.Maybe (Maybe(..), fromMaybe)
+import JSURI (encodeURIComponent)
+import Node.URL (URL, new', pathname)
+import Affjax.RequestHeader (RequestHeader(..))
+import Foreign.Object as Object
+import Data.Argonaut.Core (toObject, toArray, toString)
+import Data.Array ((!!))
+import Data.Nullable (Nullable, toMaybe)
-- Types
type Request = IncomingMessage IMServer
type Response = ServerResponse
-type ProxyConfig =
- { port :: Int
- , listenBrainzUrl :: String
- }
-
listenBrainzUrl :: String
listenBrainzUrl = "https://api.listenbrainz.org/1/user/mtmn/listens"
@@ -77,16 +84,23 @@ indexHtml =
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>scrobbler.mtmn.name</title>
- <script src="https://unpkg.com/htmx.org@2.0.7"></script>
+ <link rel="preconnect" href="https://fonts.googleapis.com">
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+ <link href="https://fonts.googleapis.com/css2?family=Intel+One+Mono:wght@300;400;500;700&display=swap" rel="stylesheet">
<style>
body {
- font-family: 'Courier New', 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
- background: #332d38;
+ font-family: 'Intel One Mono', 'Courier New', 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+ background: #000000;
color: #ffffff;
margin: 0;
padding: 20px;
line-height: 1.6;
}
+
+ ::selection {
+ background: #50447f;
+ color: #ffffff;
+ }
.container {
max-width: 800px;
@@ -97,6 +111,9 @@ indexHtml =
color: #ffffff;
margin-bottom: 20px;
font-size: 24px;
+ border-bottom: 2px solid #50447f;
+ display: inline-block;
+ padding-bottom: 5px;
}
ul {
@@ -106,19 +123,20 @@ indexHtml =
}
li {
- background: rgba(0, 0, 0, 0.3);
- border: 1px solid rgba(80, 68, 127, 0.5);
+ background: #521e40;
+ border: 1px solid #50447f;
border-radius: 4px;
padding: 15px;
margin-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: center;
+ box-shadow: 4px 4px 0px #50447f;
}
li.success {
- background: rgba(185, 208, 170, 0.2);
- border-color: rgba(185, 208, 170, 0.3);
+ background: #521e40;
+ border-color: #50447f;
}
.track-info {
@@ -134,7 +152,7 @@ indexHtml =
.track-artist {
font-size: 14px;
color: #a0c0d0;
- margin-top: 4px;
+ margin-top: 1px;
}
.track-time {
@@ -142,11 +160,29 @@ indexHtml =
color: #9fbfe7;
margin-top: 2px;
}
+
+ .album-link {
+ color: #9fbfe7;
+ text-decoration: underline;
+ }
+
+ .album-link:hover {
+ color: #ffffff;
+ }
.status {
color: #b9d0aa;
font-weight: bold;
}
+
+ .track-cover {
+ width: 60px;
+ height: 60px;
+ margin-left: 15px;
+ border-radius: 4px;
+ object-fit: cover;
+ background: rgba(255, 255, 255, 0.05);
+ }
.loading {
padding: 20px;
@@ -207,125 +243,25 @@ indexHtml =
</style>
</head>
<body>
- <div class="container">
- <h1>History</h1>
- <ul id="tracks-container"
- hx-get="/proxy"
- hx-trigger="load, every 30s"
- hx-target="#tracks-container"
- hx-ext="listenbrainz">
- <li class="loading">Loading recent tracks...</li>
- </ul>
-
- <p class="small">
- <a href="https://listenbrainz.org/user/mtmn/">ListenBrainz</a>
- </p>
-
- <p class="small" id="last-updated"></p>
- </div>
-
- <script>
- // Update last updated time
- function updateLastUpdated() {
- const now = new Date();
- document.getElementById('last-updated').textContent = `${now.toISOString()}`;
- }
-
- // Handle ListenBrainz API response
- htmx.defineExtension('listenbrainz', {
- onEvent: function(name, evt) {
- if (name === 'htmx:afterRequest') {
- const xhr = evt.detail.xhr;
- if (xhr.status === 200) {
- try {
- const data = JSON.parse(xhr.responseText);
- if (data.payload && data.payload.listens) {
- const html = generateTracksHTML(data.payload.listens);
- document.getElementById('tracks-container').innerHTML = html;
- } else {
- document.getElementById('tracks-container').innerHTML = '<li class="error">No tracks found</li>';
- }
- } catch (e) {
- document.getElementById('tracks-container').innerHTML = '<li class="error">Error parsing response</li>';
- }
- } else {
- document.getElementById('tracks-container').innerHTML = '<li class="error">Error loading tracks</li>';
- }
- updateLastUpdated();
- }
- }
- });
-
- function generateTracksHTML(listens) {
- let html = '';
- listens.forEach((listen, index) => {
- const track = listen.track_metadata;
- const listenedAt = formatTimeAgo(listen.listened_at);
-
- const trackName = track.track_name || 'Unknown Track';
- const artistName = track.artist_name || 'Unknown Artist';
- const releaseName = track.release_name || 'Unknown Album';
-
- const playingIndicator = index === 0 ? '<span class="playing-indicator"></span>' : '';
-
- html += `
- <li class="success">
- <div class="track-info">
- <div class="track-name">${playingIndicator}${escapeHtml(trackName)}</div>
- <div class="track-artist">${escapeHtml(artistName)}</div>
- <div class="track-time">${escapeHtml(releaseName)} • ${listenedAt}</div>
- </div>
- <span class="status">Played</span>
- </li>
- `;
- });
- return html;
- }
-
- function formatTimeAgo(timestamp) {
- const now = Math.floor(Date.now() / 1000);
- const diff = now - timestamp;
-
- if (diff < 60) {
- return 'just now';
- } else if (diff < 3600) {
- const minutes = Math.floor(diff / 60);
- return `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
- } else if (diff < 86400) {
- const hours = Math.floor(diff / 3600);
- return `${hours} hour${hours > 1 ? 's' : ''} ago`;
- } else {
- const days = Math.floor(diff / 86400);
- return `${days} day${days > 1 ? 's' : ''} ago`;
- }
- }
-
- function escapeHtml(text) {
- const map = {
- '&': '&',
- '<': '<',
- '>': '>',
- '"': '"',
- "'": '''
- };
- return text.replace(/[&<>"']/g, m => map[m]);
- }
-
- // Initial update
- updateLastUpdated();
- </script>
+ <div id="app"></div>
+ <script src="/client.js"></script>
</body>
</html>"""
-- Request handler
handleRequest :: Request -> Response -> Effect Unit
handleRequest req res = do
- let path = IM.url req
+ let rawUrl = IM.url req
+ url <- new' rawUrl "http://localhost"
+ path <- pathname url
Console.log $ "Request received: " <> path
case path of
"/" -> serveIndex res
"/proxy" -> serveProxy res
+ "/discogs-cover" -> serveDiscogsCover url res
+ "/lastfm-cover" -> serveLastfmCover url res
+ "/client.js" -> serveClientJs res
"/favicon.ico" -> serveFavicon res
_ -> serveNotFound res
@@ -333,13 +269,128 @@ serveIndex :: Response -> Effect Unit
serveIndex res = do
setHeader "Content-Type" "text/html" (toOutgoingMessage res)
setHeader "Access-Control-Allow-Origin" "*" (toOutgoingMessage res)
- setHeader "Access-Control-Allow-Methods" "GET, POST, OPTIONS" (toOutgoingMessage res)
- setHeader "Access-Control-Allow-Headers" "*" (toOutgoingMessage res)
setStatusCode 200 res
let w = toWriteable (toOutgoingMessage res)
void $ writeString w UTF8 indexHtml
end w
+serveClientJs :: Response -> Effect Unit
+serveClientJs res = do
+ setHeader "Content-Type" "application/javascript" (toOutgoingMessage res)
+ setHeader "Access-Control-Allow-Origin" "*" (toOutgoingMessage res)
+ launchAff_ do
+ result <- try $ FSA.readTextFile UTF8 "client.js"
+ liftEffect $ case result of
+ Right content -> do
+ setStatusCode 200 res
+ let w = toWriteable (toOutgoingMessage res)
+ void $ writeString w UTF8 content
+ end w
+ Left _ -> serveNotFound res
+
+foreign import getQueryParam :: String -> URL -> Effect (Nullable String)
+
+serveLastfmCover :: URL -> Response -> Effect Unit
+serveLastfmCover url res = do
+ launchAff_ do
+ artistMaybe <- liftEffect $ getQueryParam "artist" url
+ releaseMaybe <- liftEffect $ getQueryParam "release" url
+ let artistStr = fromMaybe "" (toMaybe artistMaybe)
+ let releaseStr = fromMaybe "" (toMaybe releaseMaybe)
+
+ env <- liftEffect getEnv
+ let apiKey = Object.lookup "LASTFM_API_KEY" env
+ case apiKey of
+ Nothing -> liftEffect $ serveNotFound res
+ Just k -> do
+ let searchUrl = "https://ws.audioscrobbler.com/2.0/?method=album.getinfo&api_key=" <> k <> "&artist=" <> (fromMaybe "" $ encodeURIComponent artistStr) <> "&album=" <> (fromMaybe "" $ encodeURIComponent releaseStr) <> "&format=json"
+ liftEffect $ Console.log $ "Last.fm search: " <> artistStr <> " - " <> releaseStr
+
+ result <- try $ fetch searchUrl { method: GET }
+ case result of
+ Right fetchRes -> do
+ json <- fromJson fetchRes.json
+ let coverUrl = do
+ obj <- toObject json
+ album <- Object.lookup "album" obj >>= toObject
+ images <- Object.lookup "image" album >>= toArray
+ -- Try to get the large image (index 2 or 3)
+ imageObj <- images !! 2 >>= toObject
+ u <- Object.lookup "#text" imageObj >>= toString
+ if u == "" then Nothing else Just u
+
+ case coverUrl of
+ Just urlStr -> do
+ liftEffect $ Console.log $ "Found Last.fm cover: " <> urlStr
+ proxyImage urlStr res
+ Nothing -> do
+ liftEffect $ Console.log $ "No Last.fm cover found for: " <> artistStr <> " - " <> releaseStr
+ liftEffect $ serveNotFound res
+ Left err -> do
+ liftEffect $ Console.log $ "Last.fm API error: " <> Exception.message err
+ liftEffect $ serveNotFound res
+
+serveDiscogsCover :: URL -> Response -> Effect Unit
+serveDiscogsCover url res = do
+ launchAff_ do
+ artistMaybe <- liftEffect $ getQueryParam "artist" url
+ releaseMaybe <- liftEffect $ getQueryParam "release" url
+
+ let artistStr = fromMaybe "" (toMaybe artistMaybe)
+ let releaseStr = fromMaybe "" (toMaybe releaseMaybe)
+
+ env <- liftEffect getEnv
+ let token = Object.lookup "DISCOGS_TOKEN" env
+ case token of
+ Nothing -> do
+ liftEffect $ Console.log "DISCOGS_TOKEN not found in env"
+ liftEffect $ serveNotFound res
+ Just t -> do
+ let queryStr = artistStr <> " " <> releaseStr
+ let searchUrl = "https://api.discogs.com/database/search?q=" <> (fromMaybe "" $ encodeURIComponent queryStr) <> "&type=release&per_page=1&token=" <> t
+ liftEffect $ Console.log $ "Discogs search (broad): " <> queryStr
+
+ result <- try $ fetch searchUrl { method: GET, headers: { "User-Agent": "ScrobblerPureScript/1.0" } }
+ case result of
+ Right fetchRes -> do
+ json <- fromJson fetchRes.json
+ let coverUrl = do
+ obj <- toObject json
+ results <- Object.lookup "results" obj >>= toArray
+ firstResult <- results !! 0 >>= toObject
+ cover <- Object.lookup "cover_image" firstResult >>= toString
+ pure cover
+
+ case coverUrl of
+ Just urlStr -> do
+ liftEffect $ Console.log $ "Found Discogs cover: " <> urlStr
+ proxyImage urlStr res
+ Nothing -> do
+ liftEffect $ Console.log $ "No Discogs cover found for: " <> queryStr
+ liftEffect $ serveNotFound res
+ Left err -> do
+ liftEffect $ Console.log $ "Discogs API error: " <> Exception.message err
+ liftEffect $ serveNotFound res
+
+proxyImage :: String -> Response -> Aff Unit
+proxyImage urlStr res = liftEffect $ do
+ imgReq <- HTTPS.get urlStr
+ imgReq # on_ Client.responseH \imgRes -> do
+ let imgSc = IM.statusCode imgRes
+ setStatusCode imgSc res
+ setHeader "Content-Type" (fromMaybe "image/jpeg" $ Object.lookup "content-type" (IM.headers imgRes)) (toOutgoingMessage res)
+ setHeader "Cache-Control" "public, max-age=86400" (toOutgoingMessage res)
+
+ let reader = IM.toReadable imgRes
+ let writer = toWriteable (toOutgoingMessage res)
+ void $ pipe reader writer
+
+ let errorH = EventHandle "error" mkEffectFn1
+ on_ errorH (\err -> do
+ Console.log $ "Image proxy error: " <> Exception.message err
+ serveNotFound res
+ ) (unsafeCoerce imgReq)
+
serveProxy :: Response -> Effect Unit
serveProxy res = do
setHeader "Content-Type" "application/json" (toOutgoingMessage res)
@@ -383,6 +434,9 @@ startServer port = do
listenTcp netServer { host: "127.0.0.1", port, backlog: 128 }
+foreign import dotenvConfig :: Effect Unit
+
main :: Effect Unit
main = do
+ dotenvConfig
startServer 8000
diff --git a/src/Types.purs b/src/Types.purs
@@ -0,0 +1,82 @@
+module Types where
+
+import Prelude
+
+import Data.Argonaut (class DecodeJson, decodeJson, (.:), (.:?))
+import Data.Maybe (Maybe)
+import Data.Generic.Rep (class Generic)
+
+newtype ListenBrainzResponse = ListenBrainzResponse
+ { payload :: Payload
+ }
+
+derive instance eqListenBrainzResponse :: Eq ListenBrainzResponse
+derive instance genericListenBrainzResponse :: Generic ListenBrainzResponse _
+
+instance DecodeJson ListenBrainzResponse where
+ decodeJson json = do
+ obj <- decodeJson json
+ payload <- obj .: "payload"
+ pure $ ListenBrainzResponse { payload }
+
+newtype Payload = Payload
+ { listens :: Array Listen
+ }
+
+derive instance eqPayload :: Eq Payload
+derive instance genericPayload :: Generic Payload _
+
+instance DecodeJson Payload where
+ decodeJson json = do
+ obj <- decodeJson json
+ listens <- obj .: "listens"
+ pure $ Payload { listens }
+
+newtype Listen = Listen
+ { trackMetadata :: TrackMetadata
+ , listenedAt :: Maybe Int
+ }
+
+derive instance eqListen :: Eq Listen
+derive instance genericListen :: Generic Listen _
+
+instance DecodeJson Listen where
+ decodeJson json = do
+ obj <- decodeJson json
+ trackMetadata <- obj .: "track_metadata"
+ listenedAt <- obj .:? "listened_at"
+ pure $ Listen { trackMetadata, listenedAt }
+
+newtype TrackMetadata = TrackMetadata
+ { trackName :: Maybe String
+ , artistName :: Maybe String
+ , releaseName :: Maybe String
+ , mbidMapping :: Maybe MbidMapping
+ }
+
+derive instance eqTrackMetadata :: Eq TrackMetadata
+derive instance genericTrackMetadata :: Generic TrackMetadata _
+
+instance DecodeJson TrackMetadata where
+ decodeJson json = do
+ obj <- decodeJson json
+ trackName <- obj .:? "track_name"
+ artistName <- obj .:? "artist_name"
+ releaseName <- obj .:? "release_name"
+ mbidMapping <- obj .:? "mbid_mapping"
+ pure $ TrackMetadata { trackName, artistName, releaseName, mbidMapping }
+
+newtype MbidMapping = MbidMapping
+ { releaseMbid :: Maybe String
+ , caaReleaseMbid :: Maybe String
+ }
+
+derive instance eqMbidMapping :: Eq MbidMapping
+derive instance genericMbidMapping :: Generic MbidMapping _
+
+instance DecodeJson MbidMapping where
+ decodeJson json = do
+ obj <- decodeJson json
+ releaseMbid <- obj .:? "release_mbid"
+ caaReleaseMbid <- obj .:? "caa_release_mbid"
+ pure $ MbidMapping { releaseMbid, caaReleaseMbid }
diff --git a/test/Main.purs b/test/Main.purs
@@ -0,0 +1,11 @@
+module Test.Main where
+
+import Prelude
+
+import Effect (Effect)
+import Effect.Class.Console (log)
+
+main :: Effect Unit
+main = do
+ log "🍝"
+ log "You should add some tests."