soulseek: fix browse crashing on the wrapped share-tree response

GET /users/{u}/browse returns { directories, directoryCount, lockedDirectories,
lockedDirectoryCount }, not the bare Directory[] that 0.26.0's source suggests,
so spreading it threw "E is not iterable" and every browse failed. Read
.directories, tolerating both shapes since the two disagree.

Also note in the type that browsed files carry only a basename, unlike search
results which carry the full remote path slskd needs to enqueue.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-29 23:47:50 +00:00
co-authored by Claude Opus 4.8
parent ef541bbc1b
commit df3643612c
2 changed files with 14 additions and 1 deletions
@@ -7,6 +7,7 @@ import { useSoulseekFavorites } from './useSoulseekFavorites';
import {
SOULSEEK_USER_CHANNEL,
type SlskdBrowseDirectory,
type SlskdBrowseResponse,
type SlskdUserInfo,
type SlskdUserStatus,
type SoulseekUserRequest,
@@ -51,9 +52,11 @@ export const SoulseekUsers = () => {
const browse = async (username: string) => {
setBrowsing(true);
try {
const tree = await client.get<SlskdBrowseDirectory[]>(
const res = await client.get<SlskdBrowseResponse | SlskdBrowseDirectory[]>(
`/slskd/api/v0/users/${encodeURIComponent(username)}/browse`,
);
// Tolerate both shapes: 0.26.0's source returns a bare array, the running build wraps it.
const tree = Array.isArray(res) ? res : (res?.directories ?? []);
setDirs([...tree].sort((a, b) => a.name.localeCompare(b.name)));
} catch (err) {
toast.error(`Browse failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -174,6 +174,16 @@ export type SlskdUserStatus = {
presence?: string; // 'Offline' | 'Away' | 'Online'
};
export type SlskdBrowseDirectory = { name: string; fileCount: number; files?: SlskdFile[] };
// GET /users/{u}/browse wraps the share tree rather than returning a bare array, and inlines every
// file of every folder in one shot (real peers hit 18k folders / tens of MB). Note that a browsed
// file's `filename` is only the basename — unlike search results, where it's the full remote path —
// so enqueuing one means rejoining it to its directory name with a backslash.
export type SlskdBrowseResponse = {
directories: SlskdBrowseDirectory[];
directoryCount: number;
lockedDirectories: SlskdBrowseDirectory[];
lockedDirectoryCount: number;
};
// Server connection state (GET /server, and the server block of GET /application).
export type SlskdServerState = { address?: string; state?: string; isConnected?: boolean; username?: string };