soulseek, extracted from the platform as a plugin
Seventh plugin out, and the first that was already dead. Soulseek had been switched off since 2026-08-13: hono.ts had the router import and its mount commented out, so createSidecarProxy never ran, registerProxiedPrefix was never called, the slskd:server announcement fired into a void, and officer-slskd was not in the ecosystem at all. The permission entry had been reduced to api: [], which is why assertPermissionTotality was satisfied and the server booted clean. The UI rendered perfectly and every request 404'd. So this is a resurrection as well as a move. The mount is now /api/soulseek, derived via mountPrefix from the appName — all 47 frontend call sites repointed from /slskd/. The credential key in service_connections stays 'slskd': it names the daemon, not the route. Those two strings differ on purpose and conflating them reads the wrong row. The connection stays in core service_connections, as transmission does, so 'slskd' remains in a platform union this plugin cannot extend. Recorded in PLUGIN.md. Unlike transmission this plugin does own a schema: favourites and the two halves of the cached browse tree, which slskd has no concept of. Those three tables had been dropped from the drizzle barrel, so bun db:push would have deleted them on any server holding data. 313 palette classes converted to the semantic tone set, including a shared PHASE_TONE the three transfer views previously each copied.
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
|
||||
const KEY = ['soulseek', 'favorites'] as const;
|
||||
const EMPTY: string[] = [];
|
||||
|
||||
/**
|
||||
* The owner's favourited Soulseek peers, backed by the officer-slskd sidecar's own `/_officer/favorites`
|
||||
* route (slskd has no favourites API — Officer owns that data). One shared react-query cache, so every
|
||||
* star in the workspace reflects the same state; toggling is optimistic and rolls back on failure.
|
||||
*/
|
||||
export function useSoulseekFavorites() {
|
||||
const { get, post, delete: del } = useClient();
|
||||
const qc = useQueryClient();
|
||||
|
||||
const { data } = useQuery({
|
||||
queryKey: KEY,
|
||||
queryFn: () => get<string[]>('/soulseek/_officer/favorites'),
|
||||
staleTime: 60_000,
|
||||
});
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: ({ on, username }: { on: boolean; username: string }) =>
|
||||
on
|
||||
? post('/soulseek/_officer/favorites', { username })
|
||||
: del(`/soulseek/_officer/favorites?username=${encodeURIComponent(username)}`),
|
||||
onMutate: async ({ on, username }) => {
|
||||
await qc.cancelQueries({ queryKey: KEY });
|
||||
const prev = qc.getQueryData<string[]>(KEY) ?? EMPTY;
|
||||
const next = on ? [...prev, username].sort((a, b) => a.localeCompare(b)) : prev.filter((u) => u !== username);
|
||||
qc.setQueryData<string[]>(KEY, next);
|
||||
return { prev };
|
||||
},
|
||||
onError: (_err, _vars, ctx) => {
|
||||
if (ctx?.prev) qc.setQueryData(KEY, ctx.prev);
|
||||
},
|
||||
});
|
||||
|
||||
const favorites = data ?? EMPTY;
|
||||
const isFavorite = (username: string) => favorites.includes(username);
|
||||
const toggle = (username: string) => {
|
||||
if (!username) return;
|
||||
mutation.mutate({ on: !isFavorite(username), username });
|
||||
};
|
||||
|
||||
return { favorites, isFavorite, toggle };
|
||||
}
|
||||
Reference in New Issue
Block a user