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>
106 lines
6.3 KiB
TypeScript
106 lines
6.3 KiB
TypeScript
import type { PluginManifest } from '@@/plugins/manifest';
|
|
|
|
// Music — the library, the player, and the phone and tablet apps that stream from it.
|
|
//
|
|
// The second plugin extracted from the platform, on 2026-08-15. Bigger than offscale and, unlike it, not
|
|
// a clean cut. It took two passes: the first left three pieces in the platform, and the second moved two
|
|
// of them here after the owner read the code and asked why the platform still had files named for music.
|
|
// He was right — one of the three "seams" turned out to be dead code holding the door open.
|
|
//
|
|
// api/router.ts the sidecar proxy, built here — thin, and it must never grow music knowledge
|
|
// cliamp/ the second playback path, parked
|
|
// widgets/ the dashboard widget, parked
|
|
// assets/icon.png the dock tile, published to /plugins/music/ on install
|
|
// sidecar/ the whole /api/music contract: indexing, streaming, per-user state
|
|
// db/ music_favorites, _playlists, _playlist_items, _now_playing
|
|
// web/ the library panels; the shell renders the Workspace
|
|
//
|
|
// ── What stayed in the platform, and why ── (nothing. All three moved here.)
|
|
//
|
|
// 1. ~~cliamp~~ — MOVED HERE, all of it, into `./cliamp/`. The sidecar halves, the relay, the file
|
|
// browser's panel and its `Play` action. `src/servers/sidecar/music/`, `src/servers/api/cliamp/` and
|
|
// `src/servers/api/music/` no longer exist, and `server.tsx` has no cliamp anything. It is PARKED, not
|
|
// working: bringing it back needs a plugin to own a websocket, which is a platform gap.
|
|
//
|
|
// 2. ~~The dashboard widget~~ — MOVED HERE, to `./widgets/`, and unregistered from WidgetRegistry.
|
|
// Parked: plugins cannot contribute widgets and that mechanism is not built.
|
|
//
|
|
// 3. ~~The global player overlay~~ — MOVED HERE, to `./web/`. It stayed while the widget pinned
|
|
// `useMusicPlayer` in `officerdev`; once the widget left, the only platform dependency was one line
|
|
// in DashboardLayout. `MusicPlayerHost` now mounts inside the MusicDetail panel, where it owns the
|
|
// audio engine and renders nothing — which is what it already did on /music.
|
|
//
|
|
// `[phase 2]` Leaving /music stops playback. Giving audio a life outside the route needs a shell slot
|
|
// a plugin can contribute to, or the engine hoisted to module scope. Deferred deliberately; nothing
|
|
// breaks meanwhile.
|
|
//
|
|
// ── Host dependencies ──
|
|
//
|
|
// Music is the plugin that made `osDependencies` exist. Offscale was self-sufficient, so until this one
|
|
// there was nothing to declare and no reason to build the field — see ./PLUGIN.md.
|
|
export const manifest: PluginManifest = {
|
|
publisher: 'officerdev',
|
|
version: '2026.08.27.01',
|
|
platform: '>=1.0.0',
|
|
|
|
label: 'Music',
|
|
summary: 'The music library — browse, play, favourites and playlists',
|
|
/** your own library. */
|
|
tags: ['Homelab', 'Personal'],
|
|
// No `icon` field: this plugin ships `assets/icon.png` and the file wins. A lucide name could only
|
|
// ever pick from the 106 glyphs the platform happens to bundle, which is a ceiling a plugin from a
|
|
// marketplace cannot see coming — and this one's artwork is a voxel duck in headphones, not a glyph.
|
|
color: '#22c55e',
|
|
|
|
// One permission gating the whole surface, grantable per role at read or write like every other.
|
|
//
|
|
// The key is `music` and that is not incidental: it is the key the platform's own registry used until
|
|
// this extraction, so every existing `role_permissions` grant keeps meaning what it meant, and the
|
|
// overlay's `can('music')` keeps resolving. Renaming it would have been a silent data change.
|
|
//
|
|
// `[open]` What a member's grant MEANS here is this plugin's own job and is not finished. Favourites,
|
|
// playlists and now-playing are already per-caller — the sidecar scopes every one of them by the
|
|
// `X-Officer-User` header the proxy injects — while the library itself is one shared index for the
|
|
// household. So "whose row is this" already has a real, non-uniform answer, which is why the platform's
|
|
// side of it is a uniform read/write and nothing more. Designing the rest belongs in these queries.
|
|
permissions: [
|
|
{
|
|
key: 'music',
|
|
label: 'Music',
|
|
description: 'The music library, playback, and your own favourites and playlists',
|
|
// These are the `personal` paths from the registry entry this replaces, carried across verbatim.
|
|
//
|
|
// They are not read-only — they are genuine writes to the CALLER'S own data, which is what made
|
|
// them safe at read level. The manifest deliberately has no `personal` field, and adding one would
|
|
// be designing the per-user visibility model that is explicitly not this extraction's work. It
|
|
// costs nothing to go without: `isRequestAllowedAtLevel` concatenates `personal` and
|
|
// `readOnlyWrites` into a single allow-list, so the two are the same mechanism under two names and
|
|
// a read grant permits exactly the same four paths it permitted yesterday.
|
|
//
|
|
// `/queue` is here because it was there. No such route exists, in the sidecar or anywhere else.
|
|
readOnlyWrites: ['/favorites', '/now-playing', '/playlists', '/queue'],
|
|
},
|
|
],
|
|
|
|
// Both come from one package everywhere, which is luck rather than a rule — hence a name per manager
|
|
// rather than one canonical name. `packages.sh` records why that indirection was rejected.
|
|
//
|
|
// They are declared SEPARATELY even so, because the platform probes binaries and these two fail
|
|
// differently. Losing `ffprobe` is the quiet one: the indexer catches the spawn error and returns a
|
|
// track carrying its filename and nothing else — no title, artist, album, duration or embedded
|
|
// lyrics — then reports success. Losing `ffmpeg` costs cover art and video poster frames, which is at
|
|
// least visible. Naming both means the owner is told which of the two they are missing.
|
|
osDependencies: [
|
|
{
|
|
binary: 'ffprobe',
|
|
reason: 'Reads tags, duration and embedded lyrics. Without it every track indexes as a bare filename.',
|
|
packages: { apt: 'ffmpeg', pacman: 'ffmpeg', dnf: 'ffmpeg', brew: 'ffmpeg' },
|
|
},
|
|
{
|
|
binary: 'ffmpeg',
|
|
reason: 'Compresses cover art for phones and grabs poster frames from videos.',
|
|
packages: { apt: 'ffmpeg', pacman: 'ffmpeg', dnf: 'ffmpeg', brew: 'ffmpeg' },
|
|
},
|
|
],
|
|
};
|