{"id":51849900,"url":"https://github.com/felipedbene/casquinha","last_synced_at":"2026-07-23T17:30:34.160Z","repository":{"id":369781199,"uuid":"1290453451","full_name":"felipedbene/casquinha","owner":"felipedbene","description":"A native Mac OS 9 (PowerPC) Spotify remote — controls a gopher-spot backend over raw gopher (RFC 1436) and decodes a live Icecast MP3 stream in-app.","archived":false,"fork":false,"pushed_at":"2026-07-19T15:00:50.000Z","size":4723,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-07-19T15:27:34.598Z","etag":null,"topics":["c99","classic-mac","gopher","icecast","mac-os-9","open-transport","powerpc","retrocomputing","spotify","spotify-connect"],"latest_commit_sha":null,"homepage":"https://debene.dev","language":"C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/felipedbene.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-07-06T00:47:19.000Z","updated_at":"2026-07-19T15:00:59.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/felipedbene/casquinha","commit_stats":null,"previous_names":["felipedbene/casquinha"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/felipedbene/casquinha","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/felipedbene%2Fcasquinha","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/felipedbene%2Fcasquinha/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/felipedbene%2Fcasquinha/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/felipedbene%2Fcasquinha/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/felipedbene","download_url":"https://codeload.github.com/felipedbene/casquinha/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/felipedbene%2Fcasquinha/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35811417,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-07-20T02:08:10.276Z","status":"online","status_checked_at":"2026-07-23T02:00:06.683Z","response_time":57,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["c99","classic-mac","gopher","icecast","mac-os-9","open-transport","powerpc","retrocomputing","spotify","spotify-connect"],"created_at":"2026-07-23T17:30:33.444Z","updated_at":"2026-07-23T17:30:34.154Z","avatar_url":"https://github.com/felipedbene.png","language":"C","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Casquinha\n\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) ![Platform](https://img.shields.io/badge/Mac%20OS-9-black.svg) ![Arch](https://img.shields.io/badge/PowerPC-C99-555.svg)\n\n\u003cimg src=\"design/icon/casquinha-icon.png\" alt=\"Casquinha icon\" width=\"96\"\u003e\n\n![Casquinha running on Mac OS 9.2 — now playing over gopher-spot, with search and queue](design/screenshot.png)\n\nA native **Mac OS 9.2** Spotify remote — *the essential Radinho, on the oldest\nmachine yet* — speaking the frozen\n**[gopher-spot](https://github.com/felipedbene/gopher-spot) machine API\n`/spot/api/1`** over raw gopher (RFC 1436), LAN-only. It is the PowerPC / Classic\nsibling of\n[DeToca](https://github.com/felipedbene/detoca) (Snow Leopard 10.6 / i386) and\n[DeGelato](https://github.com/felipedbene/degelato) (Sorbet Leopard 10.5 / ppc),\nand the third reference implementation of the umbrella recipe,\n[**fhb ▸ CLIENT-PATTERN.md**](https://github.com/felipedbene/fhb/blob/main/CLIENT-PATTERN.md).\n\nThe pattern's whole claim is that the hard part — staying *coherent* against a\nreplicated, eventually-consistent backend over a protocol that cannot un-send a\ncommand — lives in a handful of **pure** layers and seven reconciliation laws\nthat come from the backend and the protocol, **not the OS**. So they port\nverbatim; only the glue and the platform escape-hatches change. Casquinha is that\nclaim taken to its limit: no Cocoa, no Objective-C, no BSD sockets, no threads —\njust plain C, **Open Transport**, and the **Toolbox** on a cooperative event loop.\n\n## What it does\n\nA full native remote for the gopher-spot bridge, driving Spotify and showing\nstate entirely in the Toolbox:\n\n- **Now Playing** — a Platinum window polling `/now` every 2 s over Open\n  Transport, interpolating the progress bar from `ts` between polls, with a\n  monotonic guard so a stale reply never rewinds the display. TEC UTF-8→MacRoman\n  for accented names; graceful 429 backoff.\n- **Transport** — prev / play-pause / next, a volume slider, and a click-to-seek\n  progress bar.\n- **Opens ready to play** — launching the app IS the intent: it tunes the\n  stream immediately and, off the first `/now` snapshot, fires ONE wake if\n  playback isn't already on the gopher-spot device. Zero clicks to música.\n  Hold **⌥ Option** at launch to start quiet.\n- **Wake** (⌘K) — transfer playback onto the gopher-spot librespot device so\n  the audio stream carries it, recovering the \"playing on another device\"\n  idle state (manual re-wake mid-session; the launch wake is automatic, and\n  the buttons retired in b46 — the app runs itself).\n- **Search \u0026 Queue** — built into the main window (search field + results list,\n  live queue list); double-click a search hit to play it now and continue with\n  the queue, or a queue row to play **from there onward** — both as native\n  Spotify multi-track contexts via `/spot/api/1/play/from`, so advance, next\n  and prev behave natively. Nothing playback-related lives in a menu: on a\n  cooperative OS, menu tracking freezes the app — and the audio with it.\n- **Cover art** — QuickTime GraphicsImporter, behind a fail-once cover cache.\n- **Preferences** (⌘,) — File▸Preferences opens a hand-editable config file\n  (server `host:port`) in SimpleText; edit it, save, and switch back to\n  Casquinha to apply on resume (no modal dialog). Saved to disk.\n- **Audio** (automatic; ⌘T toggles manually) — with a live, player-aware\n  status readout in radio vocabulary: `tuning in… → buffering… N% → on air`,\n  `playing out… / standing by` when upstream pauses, `waiting for Spotify…`\n  for the genuine anomaly (a **server fact** from `/spot/api/1/stream` when\n  the server has it; a receive-side heuristic on older servers) — and an\n  instant `Skipping...` acknowledgment when you hit Next/Prev. Everything\n  the player area says is decided by ONE pure, host-tested module\n  (`cq_view`), so the state word, status line and ack can't contradict each\n  other on screen. The stream is the live Icecast MP3, decoded **in-app by\n  minimp3** and played through the Sound Manager (`SndPlayDoubleBuffer` fed\n  from a PCM ring, ~3 s radio latency with graceful starvation). QuickTime\n  proved unable to open a length-less live stream on OS 9, so the whole path\n  — endless Open Transport read, MP3 frame sync, decode, output — is the\n  project's own code (see NOTES.md for the b13–b49 arc).\n\nUnder all of it: **backend-exhaustion hardening + CLIENTS.md compliance** —\norderly TCP release, suspend/resume, jittered backoff, an in-flight cap, UTF-8\nsearch encoding, `rate_limited` degradation, and a single polite queue re-poll\nafter a change. See [`design/AUDIT-backend-exhaustion.md`](design/AUDIT-backend-exhaustion.md).\n\n## Status\n\nThe pure core (Codec / Model / Reconciler) is plain C99 with an offline suite\nthat runs **on a modern Mac** — no Retro68, no emulator (`make test`, **311\nchecks green**, including decoding a captured slice of the real stream). The\napp is classic PowerPC + Open Transport + the Toolbox, cross-built with\nRetro68 and run in **UTM** (QEMU/PPC).\n\nExercised on the VM (through b63; binaries on the\n[releases page](https://github.com/felipedbene/casquinha/releases)):\nauto-start, Now Playing, transport, search, queue (add + jump with **native\nplay-from contexts** via gopher-spot's `/spot/api/1/play/from`, spec'd from\nthis repo in [`design/SPEC-play-from.md`](design/SPEC-play-from.md)), covers,\npreferences, wake, and the full audio path (sustained playback, zero\nunderruns). **Open item:** the full **Fios A–H** runtime pass on real\nhardware.\n\nSee [`NOTES.md`](NOTES.md) for the fio-by-fio arc + permanent constraints, and\n[`design/PATTERN-MAP-os9.md`](design/PATTERN-MAP-os9.md) for the DeGelato →\nMac OS 9.2 mapping.\n\n## Debugging \u0026 telemetry (opt-in)\n\nThe app is silent by default — **no log files, no network telemetry**. To\nturn the debug harness on, put a file named **`Casquinha Debug`** in the same\nfolder as the app (an empty SimpleText file is enough). With the marker\npresent:\n\n- every event is appended to **`Casquinha \u003ctag\u003e.log`** next to the app (one\n  log per build, flushed per line so it survives a freeze), and\n- each line is **mirrored live as a UDP datagram**. Two sinks exist: the\n  cluster's always-on **log-sink** (a MetalLB service at\n  `\u003clog-sink-host\u003e:5514`, deployed with gopher-spot; read it with\n  `kubectl -n gopher-spot logs -f deploy/log-sink` — lines are prefixed\n  with the sender IP so all family clients can share it), or an ad-hoc\n  `make logtail` on the dev Mac (a tiny Python listener; macOS `nc -kul`\n  latches onto the first sender and drops the rest). The marker file's\n  first line selects the target as `host:port` — put\n  `\u003clog-sink-host\u003e:5514` in it for the cluster sink.\n\nDelete the marker and the app goes quiet again. Extras in `tools/`:\n`mp3scan.c` (mount forensics: frame gaps/format flips in a captured stream),\nand two AppleScripts — `Test Casquinha` (an ordered smoke test driven over\nApple Events: the app answers `quit` and a `do script` command string) and\n`Collect Logs` (Finder copies every per-build log onto the AFP share).\n`make app` drops **versioned** binaries (`Casquinha-\u003ctag\u003e.bin/.dsk`) on the\nshare alongside the unversioned latest, and the build tag is visible in the\nstatus row — no more guessing which binary is running over there.\n\n## Building\n\n```sh\nmake test        # build + run the pure-core suite on THIS Mac (offline, no Retro68)\nmake clean\n```\n\nThe pure core is deliberately host-buildable: the seam between Transport and\nCodec is where the tests live (CLIENT-PATTERN.md §1), so the Codec/Model/\nReconciler layers compile and run against the copied fixtures with the system\n`cc`, forever, with no classic hardware in the loop.\n\nThe Mac OS 9 app itself cross-builds with the **Retro68** GCC toolchain\n(PowerPC/CFM + the Rez resource compiler + Universal Interfaces) and runs in\n**UTM** (QEMU/PPC) with shared networking, so the OS 9 guest reaches the\ngopher-spot server at `\u003cspot-host\u003e:70` outbound through the host. That target is\nwired up from Fio 3 on (`make app`, once `RETRO68=\u003ctoolchain\u003e` is set).\n\n## Network contract (v1, frozen)\n\n- Server `\u003cspot-host\u003e:70` (LAN only, plain TCP, no TLS). Write `selector\\r\\n`,\n  read to EOF. `/now` returns UTF-8 `key\u003cTAB\u003evalue` lines.\n- **Ignore unknown keys, tolerate missing ones, key off `state` first** — the API\n  is additive; surface growth must never hard-fail the client.\n- The server micro-caches `/now` (~3 s); poll at 2 s and never faster.\n\nCapture a fresh fixture from the live server:\n\n```sh\nprintf '/spot/api/1/now\\r\\n' | nc \u003cspot-host\u003e 70 \u003e tests/Fixtures/now_live.txt\n```\n\n## Layout\n\n```\nsrc/\n  cq_codec.{h,c}         raw bytes -\u003e {key:value} fields; JPEG magic-byte sniff (pure)\n  cq_now.{h,c}           immutable /now snapshot; state-first; interpolation (pure)\n  cq_track.{h,c}         item.\u003ci\u003e.* rows for /queue and /search (pure)\n  cq_guard.{h,c}         monotonic ts-guard — MANDATORY (law 2) (pure)\n  cq_debounce.{h,c}      pre-wire coalescer — cancel != un-send (law 1) (pure)\n  cq_backoff.{h,c}       exponential poll backoff + seeded jitter (pure)\n  cq_cache.{h,c}         fixed-slot FIFO cover cache; fail-once semantics (pure)\n  cq_pls.{h,c}           first stream URL from a PLS/M3U (pure; audio)\n  cq_mp3.{h,c}           MP3 frame-header parse / confirmed-frame sync (pure; audio)\n  cq_mp3dec.{h,c}        the decode seam wrapping the vendored minimp3 (pure; audio)\n  minimp3.h              vendored public-domain MP3 decoder (one TU, cq_mp3dec.c)\n  cq_transport.h         the transport seam (+ streaming mode, + UDP log mirror)\n  cq_transport_ot.c      Open Transport state machine (OS 9, sync+non-blocking)\n  cq_transport_posix.c   BSD-socket twin so the seam is host-testable\nos9/\n  casquinha.c            the app: Toolbox glue, event loop, audio engine (Retro68)\n  casquinha.r, icon.r    resources + the otter icon family\ntests/\n  *_test.c               the offline suite + a tiny runner\n  Fixtures/              now_* + queue/search + cover + stream.pls + stream.mp3\n                         (a captured slice of the real mount, decoded in the suite)\ntools/\n  mp3scan.c              mount forensics: frame gaps / format flips in a capture\n  loglisten.py           UDP log listener (make logtail)\n  *.applescript          smoke test over Apple Events + Finder log collection\ndesign/\n  AUDIT-backend-exhaustion.md   exhaustion audit + Fios A–H (UTM pass pending)\n  PATTERN-MAP-os9.md            DeGelato -\u003e Mac OS 9.2 mapping\n  SPEC-play-from.md             the cross-repo spec that became /spot/api/1/play/from\n  vm-logs/                      the b13–b38 field logs behind the audio arc\n```\n\nPrefix **CQ** / `cq_`. The name is *casquinha* — an ice-cream cone; the humblest\nscoop in the freezer, for the oldest Mac in the house.\n\n---\n### Part of the gopher constellation\n**Servers \u0026 tools:** [gopher-core](https://github.com/felipedbene/gopher-core) · [gopher-cta](https://github.com/felipedbene/gopher-cta) · [gopher-blog](https://github.com/felipedbene/gopher-blog) · [gopher-askthedeck](https://github.com/felipedbene/gopher-askthedeck) · [gopher-spot](https://github.com/felipedbene/gopher-spot) · [the-economist-epub](https://github.com/felipedbene/the-economist-epub)\n**Clients:** [casquinha](https://github.com/felipedbene/casquinha) (Mac OS 9) · [detoca](https://github.com/felipedbene/detoca) (OS X 10.6) · [degelato](https://github.com/felipedbene/degelato) (OS X 10.5 PPC) · [deburrow](https://github.com/felipedbene/deburrow) (Android)\n**Protocol notes:** [fhb](https://github.com/felipedbene/fhb)\n---\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffelipedbene%2Fcasquinha","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffelipedbene%2Fcasquinha","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffelipedbene%2Fcasquinha/lists"}