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>
81 lines
3.7 KiB
TypeScript
81 lines
3.7 KiB
TypeScript
import { pgTable, serial, integer, text, real, timestamp, index, primaryKey, uniqueIndex } from 'drizzle-orm/pg-core';
|
|
import { users } from 'officerdb/auth/schema';
|
|
|
|
// Per-user music favorites. `key` is an opaque path the app supplies and the server never interprets:
|
|
// track → homePath "Music/<rel>/<file>" (also the /stream path + RNTP queue id)
|
|
// album → music-rel "Albums/AC-DC/[1980] Back in Black"
|
|
// artist → music-rel "Albums/AC-DC"
|
|
export const musicFavorites = pgTable(
|
|
'music_favorites',
|
|
{
|
|
id: serial('id').primaryKey(),
|
|
userId: integer('user_id')
|
|
.notNull()
|
|
.references(() => users.id, { onDelete: 'cascade' }),
|
|
kind: text('kind').notNull(), // 'track' | 'album' | 'artist'
|
|
key: text('key').notNull(),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(t) => [
|
|
uniqueIndex('uq_music_favorites_user_kind_key').on(t.userId, t.kind, t.key),
|
|
index('idx_music_favorites_user_kind').on(t.userId, t.kind),
|
|
],
|
|
);
|
|
|
|
// Per-user named playlists (header) + their ordered items (below). Like favorites, an item `key` is the
|
|
// opaque track homePath "Music/<rel>/<file>" the app supplies — the server never interprets it. A playlist
|
|
// name is unique per user; items are position-ordered and MAY repeat (a track can appear twice).
|
|
export const musicPlaylists = pgTable(
|
|
'music_playlists',
|
|
{
|
|
id: serial('id').primaryKey(),
|
|
userId: integer('user_id')
|
|
.notNull()
|
|
.references(() => users.id, { onDelete: 'cascade' }),
|
|
name: text('name').notNull(),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(t) => [uniqueIndex('uq_music_playlists_user_name').on(t.userId, t.name)],
|
|
);
|
|
|
|
// Ordered track entries of a playlist. `position` is 0-based; deletes cascade from the playlist.
|
|
export const musicPlaylistItems = pgTable(
|
|
'music_playlist_items',
|
|
{
|
|
id: serial('id').primaryKey(),
|
|
playlistId: integer('playlist_id')
|
|
.notNull()
|
|
.references(() => musicPlaylists.id, { onDelete: 'cascade' }),
|
|
key: text('key').notNull(), // track homePath "Music/<rel>/<file>"
|
|
position: integer('position').notNull(),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(t) => [index('idx_music_playlist_items_playlist').on(t.playlistId, t.position)],
|
|
);
|
|
|
|
// Per-(user, device) "currently playing" for resume-across-launch: the current track + playback
|
|
// position, plus a light metadata snapshot so the resume card renders before the library index has
|
|
// synced on a fresh device. `dir` is the folder to rebuild the album queue from ('' for a cross-album
|
|
// queue). `device` is an opaque client tag ('' = default/phone, 'web' = the browser) so each client
|
|
// keeps its OWN resume state instead of stomping a shared one. One row per (user, device) — upserted;
|
|
// the app writes it throttled while playing and on pause/track-change/close.
|
|
export const musicNowPlaying = pgTable(
|
|
'music_now_playing',
|
|
{
|
|
userId: integer('user_id')
|
|
.notNull()
|
|
.references(() => users.id, { onDelete: 'cascade' }),
|
|
device: text('device').notNull().default(''),
|
|
homePath: text('home_path').notNull(),
|
|
dir: text('dir').notNull().default(''),
|
|
title: text('title').notNull().default(''),
|
|
artist: text('artist').notNull().default(''),
|
|
album: text('album').notNull().default(''),
|
|
durationSec: real('duration_sec').notNull().default(0),
|
|
positionSec: real('position_sec').notNull().default(0),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(t) => [primaryKey({ name: 'pk_music_now_playing', columns: [t.userId, t.device] })],
|
|
);
|