20 Commits
Author SHA1 Message Date
pastilhas d0282fd66d release 2026.09.23.01
Carries the /random endpoint: the server now picks shuffle tracks, so a
phone no longer flattens every album's meta into its heap to choose
twenty songs.
2026-09-23 00:16:54 +01:00
pastilhas 9fb948fe83 music: the server picks the random tracks
The app shuffled by flattening every album's meta.json in scope into one
array and choosing from it. On a hundred-thousand-track library that
meant a six-figure object array living in the phone's JS heap — marked
by every garbage collection — and copied again on each draw. Measured on
device: the JS thread never went idle during playback and the phone
reached 'serious' thermal state.

Picking twenty songs never required any of that. The manifest already
holds per-album track counts, so the choice is arithmetic over numbers
and only the albums actually picked are read.

Uniform over TRACKS, not albums: choosing an album first would make a
two-track EP as likely as a sixty-track box set, so albums are weighted
by count via a running total and a binary search. Same distribution as
shuffling everything and taking the first n, which is what a shuffle
means. Results are re-shuffled before returning, because grouping the
reads by album would otherwise hand back something sorted by album.
2026-09-23 00:14:06 +01:00
pastilhas de8949efa7 slim-embedded-art: rewrite tags that are mostly padding
The final 47 oversized files held no big art and no big lyrics - just
empty reserved space, up to 478 KB of padding against 39 bytes of
frames, all in the DJ Sets tree. The player pages through padding like
anything else. A tag past 200 KB total is rewritten regardless of what
fills it; the save already zeroes padding.
2026-08-29 08:39:14 +01:00
pastilhas f17a88be9a slim-embedded-art: drop junk lyrics frames over 50 KB
The 49 files the art pass left behind were fat with USLT, not APIC -
the worst held the complete text of Angela's Ashes (1.5 MB of Frank
McCourt) in an Eels track, some tagger malfunction. AVPlayer pages
through the whole tag before the first audio frame regardless of what
fills it, so absurd lyrics cost the same silence as absurd art. No
song has 50 KB of lyrics; anything past it is junk and is removed.
2026-08-29 07:48:07 +01:00
pastilhas 76d7ce6ccd drop the temporary stream-range log — it found its bug
28 range requests per song start on iOS turned out to be AVPlayer paging
through ~1 MB of embedded cover art in 32 KB round trips. The library
has been slimmed (23,833 files rewritten, art capped at 80 KB, ID3v1
stripped); the log served its purpose.
2026-08-29 07:36:19 +01:00
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 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
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