Commit Graph
20 Commits
Author SHA1 Message Date
pastilhas 8814f90780 slim-embedded-art: strip the ID3v1 appendix on rewrite (v1=0)
Redundant with the v2 frames, 128 bytes of legacy, and the thing whose
sync update tripped the audio guard. Applies to files being rewritten;
files already slimmed before this keep their synced v1.
2026-08-29 02:05:50 +01:00
pastilhas f63e6000f9 slim-embedded-art: the audio hash excludes a trailing ID3v1 tag
mutagen updates an existing v1 tag on save - it normalised the genre
byte 0xFF to 0x0F on Shamrocks and Shenanigans and the guard refused a
whole album of good rewrites over one metadata byte. The v1 block is
metadata, not audio; the guard now hashes strictly the frames between
the v2 tag and any trailing TAG block.
2026-08-29 01:59:22 +01:00
pastilhas e0e96d10d7 slim-embedded-art: hash the audio region from mutagen own tag parse
A tagger that writes a non-standard size field puts the header claim and
the real tag end in different places. mutagen preserves audio from where
it parsed the tag to end; the guard hashed from where the header claimed,
and refused good rewrites across a whole album. Both hashes now use the
same authority. Failed files stay untouched and a re-run picks them up.
2026-08-29 01:58:30 +01:00
pastilhas fe1405f273 slim-embedded-art: recompress oversized embedded covers across the library
iOS AVPlayer reads a streamed file head in ~32 KB range requests and cannot
reach audio until it is past the ID3 tag - a 955 KB embedded JPEG cost ~30
sequential round trips, a two-second silence Android never feels. 28% of the
library (20,487 files, 1,488 albums) carries art that size.

Keeps an embedded front cover (other users of these files see it) but at
600px/JPEG stepped to fit 80 KB, ~3 chunks. Audio region is hashed before
and after; any mismatch keeps the original. Rescues art to cover.jpg first
when the folder has none.
2026-08-29 01:36:21 +01:00
pastilhas 3368f5def9 temporary: log each stream request range, chasing the iOS 28-requests-per-song storm 2026-08-29 01:09:28 +01:00
pastilhasandClaude Opus 5 6310760fda release 2026.08.27.01
first dated version. plugins were all sitting on 1.0.0, which said nothing about
when a build was made or which of two copies is newer — the platform already
uses YYYY.MM.DD.NN and this follows it.

the version lives in manifest.ts because a plugin has no package.json; that is
the field the marketplace publishes and the installer compares.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026.08.27.01
2026-08-27 07:36:31 +00:00
pastilhas c214f11c2f AGENTS.md, so both harnesses find the same guidance
opencode discovers AGENTS.md and nothing else; claude code discovers CLAUDE.md.
this repo had neither, so an agent sent to change something here arrived with no
guidance at all under either.

it deliberately does NOT repeat README.md. that file says what the plugin is,
for someone deciding whether to install it. this one answers the other question:
i have to change something in here, what will i break.

the facts in it are read from the repository rather than recalled — the panel
keys from web/panels.ts, the tables from db/schema.ts, the permission keys, the
host binaries and the route claims from manifest.ts. presence is the declaration
everywhere in this system, so the file says what the platform will actually
conclude from these files existing.

CLAUDE.md beside it is a pointer, not a copy: two copies drift and the stale one
is the one somebody reads.
2026-08-27 07:24:14 +00:00
pastilhas 60f6864126 say what this plugin is for, in the store's words
tags are how the marketplace is browsed rather than scrolled: a list of them on
the left, the plugins that carry one on the right. a closed vocabulary
(PLUGIN_TAGS in the platform) because free text becomes homelab, home-lab, Home
Lab and selfhosted inside a month, and a filter list of synonyms is worse than
no filter at all.

declared here rather than curated in the catalogue, so a plugin can say what it
is and nobody edits a table when one ships. the marketplace caches it on sync,
exactly as it already does for label and summary.
2026-08-27 06:49:34 +00:00
pastilhasandClaude Opus 5 9f06297c64 reindex-music looked one level too deep and reported the sidecar down
It read `process.env.DATA_PATH ?? cwd/data`. DATA_PATH stopped being an env var on 2026-08-13, when
the install layout became something derived from the working directory rather than configured — so run
from the repo this looked for platform/data/music/.server, which is one level too deep, found nothing,
and said the sidecar was not running.

Imports DATA_PATH from the platform instead, so there is one derivation rather than a copy that fell
behind. Verified it resolves to /home/pastilhas/officer-dev/data.

Also corrects the usage line: the script moved into this plugin and the header still said
`bun scripts/reindex-music.ts` from the platform repo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 03:39:01 +00:00
pastilhas efc9581889 restore the three streaming improvements
reverts 0e08aef, b430a70 and 1170e41 — stream-audio.ts and MUSIC_API.md are
now byte-identical to 2a70bc1.

all three were unwound over two days chasing a mobile playback failure that
turned out to have nothing to do with this plugin. the reasoning that undid
them was elimination, not diagnosis: the duration header did not fix it, so
the etag "must" be the cause. the option never considered was that none of it
was mine, which is the one that was true.

nothing here was measured wrong. removing the per-file ffprobe from the path
before the first byte is the speedup, and it is confirmed in real use.

what the mobile client should know, unchanged from MUSIC_API.md: x-audio-duration
is gone, read durationSec from /meta instead — it arrives with the queue,
before the first stream request.
2026-08-19 17:11:26 +00:00
pastilhas 1170e411ac revert the /stream etag, cache-control and 304
playback did not come back when x-audio-duration did, so the duration header
was not the only thing i broke. the range and whole-file branches are
byte-identical to the version that worked, which leaves these three as the
only remaining difference on /stream.

a cache that keeps a validator after evicting the body sends if-none-match,
gets a 304 and has no audio. plausible, unproven, and not worth another round
trip through someone's phone to find out — the etag was an optimisation and
playback is not.

/stream is now byte-identical to the last version known to play, verified by
running both implementations side by side over no-range, closed, open and
invalid ranges and diffing status, every header and body length.

if-none-match is still accepted and ignored. re-introducing this needs a
capture of what the player actually sends.
2026-08-19 15:43:11 +00:00
pastilhas b430a702b4 keep ffprobe as the floor under the meta.json lookup
the previous commit read the duration from meta.json instead of spawning
ffprobe. correct for an indexed album and wrong everywhere else: /stream
serves any home-relative path and never required an index, so a library that
has not been indexed got no header at all. alpha is exactly that case — its
music cache is empty — which is how this was caught rather than shipped.

"always present" replaced with "present if indexed" is the same regression as
removing the header, one layer down.

so both, in order: meta.json when it can answer, ffprobe when it cannot, each
cached. verified on an indexed album (247 from meta, 0.3ms warm) and on the
same file with its index deleted (7.03 from ffprobe, 83ms cold, 0.1ms warm).
2026-08-19 15:28:54 +00:00
pastilhas 0e08aefad9 restore x-audio-duration, without the ffprobe
removing it was wrong: the mobile player stopped playing. nothing in the web
read the header and /meta already carried durationSec, so it looked dead. it
was not — it is a documented part of the contract and something depended on it.

the header is back and the ffprobe is not. the value is read from the album's
meta.json, which is the same number ffprobe produced (the indexer got it from
ffprobe at index time), cached per album and keyed on the meta file's mtime so
a reindex invalidates it. 1.5ms cold, 0.1ms warm, against 50ms warm and 315ms
cold for the process spawn.

first pass looked for meta.json beside the audio; it lives under the sidecar's
own cache root mirroring the relative path. uses metaFilePath now rather than
re-deriving it.

verified against a real file: header present and equal to meta, 304 on
whole-file revalidation, 206 on range with a matching etag.
2026-08-19 15:26:39 +00:00
officer 2a70bc18d7 three bottlenecks in streaming and loading
Measured, not guessed.

ffprobe is gone from the stream path. It ran on the first request for each
file — 50ms warm, 315ms cold on this machine, paid before the first byte, once
per file per sidecar restart — to fill X-Audio-Duration, a header nothing in
the web read and which duplicates the durationSec the indexer already wrote
into meta.json. Removed rather than cached harder: the fastest work is the work
not done, and a second copy of a number is a second thing that can disagree.
BREAKING for any client that read that header; read /meta instead, which every
client already fetches to build its queue.

/stream gains an ETag and Cache-Control. It was the only route with no
validator at all — cover, meta, lyrics and poster have had one all along — so
every seek and every replay re-read the file over the network. mtime+size
identifies the bytes, since a re-encode replaces the file. A Range request
never answers 304: that would answer a question about part of a file with a
statement about all of it, and players handle it badly.

And `?v=` makes covers cacheable rather than merely revalidatable. `no-cache`
means "ask before reusing", so a grid of forty albums cost forty conditional
requests on every load, all answering 304. The manifest already carries each
album's version, so passing it costs the client nothing — and the server then
answers `immutable`, making a repeat visit send nothing at all.

That last one is deliberately conditional on `v` being present, which keeps it
backward compatible: a client that has not been updated gets today's behaviour
and cannot end up holding a stale cover forever.
2026-08-19 15:15:35 +00:00
Claude 456336e27f art: the splash, beside the icon
Copied from its existing master — this art was already drawn to the current
brief, it simply had nowhere to live outside a designer's folder. The pair is
the point: the icon is a reduction of the splash's world.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 06:50:48 +01:00
Claude cef93989c5 icon: transparent corners, so the tile is a tile
The app icon is flattened onto black because iOS rejects an alpha channel, and
copying that straight over made every dock tile an opaque black square. The tile
wants the frame's own rounded edge with the corners cut away — which is what the
icon this replaces already had, and what tile: bare means.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 06:28:30 +01:00
Claude ed2464b5c3 icon: the app's own artwork, at 256
The dock tile takes assets/icon.png when the plugin ships one and falls back to
the manifest's lucide glyph when it does not. This is the same icon the phone
shows, resized — one mark per product, wherever it appears.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 06:03:59 +01:00
pastilhasandClaude Opus 5 3f4ee82ab6 cliamp: validate the path before checking the host for the binary
openPlayer checked `findCliamp()` before `resolveInHome(files)`, so a path
escaping the owner's home was answered "cliamp not found on host" on any
machine without the binary — telling the caller about the host in response to
input that should simply have been refused.

It also made the guarantee depend on an unrelated binary being installed,
which is why cliamp-ws.test.ts's "rejects a path that escapes the owner home"
failed on this machine (and every other one without cliamp) since the tests
started running from plugins/ at all. It was being carried as a known-failing
baseline rather than read.

Validate the request, then inspect the host. No behavioural change on a
machine that has cliamp.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 13:49:59 +00:00
pastilhas 5bd27838bd readme: what music does, what it needs, and what it does not do yet 2026-08-16 04:07:20 +00:00
Claude Opus 5 6e07da7a36 music, extracted from the platform into its own repository
Everything the plugin is, moved out of officerdev/platform on 2026-08-15 —
41 files, unchanged from the tree they left.

  manifest.ts   identity, one permission, ffmpeg/ffprobe declared
  api/          the sidecar proxy; the prefix comes from mountPrefix()
  sidecar/      the whole /api/music contract — indexing, streaming, per-user state
  db/           music_favorites, _playlists, _playlist_items, _now_playing
  web/          panels, layout, and the player: engine, bar, lyrics, favourites
  cliamp/       the second playback path, parked — not working, kept deliberately
  widgets/      the dashboard widget, parked — plugins cannot contribute widgets
  assets/       icon.png, the dock tile
  scripts/      the reindex CLI

PLUGIN.md is the design record: what moved, what stayed, what broke, and why.
MUSIC_API.md is the contract the phone and tablet apps speak, and the reason
the sidecar's HTTP shape is not free to change.

── It does not build here, and that is the point ──

The platform resolves `hooks/useClient`, `officerdev`, `officerdb/db` and `@@/*`
through the workspace links in its own node_modules. Measured from this
directory, outside the platform checkout, every one of them fails to resolve —
7 imports in the backend, ~29 in the frontend.

So this repository is the source of truth, not yet a buildable unit. Making it
one means the host API becoming something a plugin can depend on rather than
something it reaches into. That is the next problem, and having the code here
is what makes it unavoidable rather than theoretical.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 17:34:51 +00:00