soulseek: queue downloads straight out of the cached tree

a browsed folder is a path, not a file list, and what clicking one means is
"everything under here" — so the expansion happens in the sidecar, off the
cache, rather than making the browser walk the tree a level at a time and
rebuild paths it only half knows.

browse reports file names as basenames, unlike search, so the peer's real
path is rejoined from the folder row. sizes come from the cache too: slskd
matches a queued download on filename AND size, so a number supplied by the
client would be a transfer that silently never starts.

a subtree can be the peer's whole share (284k files on one measured peer), so
an over-limit request is refused with its count rather than truncated into a
partial download nobody asked for.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 01:57:01 +00:00
co-authored by Claude Opus 4.8
parent b94dccd17c
commit 184adc0e8e
6 changed files with 215 additions and 36 deletions
+60
View File
@@ -0,0 +1,60 @@
import { getSoulseekBrowseDownload } from 'officerdb';
import { getSlskdBase, getSlskdApiKey } from './upstream';
// Enqueueing downloads out of a cached share tree.
//
// slskd's own enqueue endpoint takes a flat list of {filename, size}, which is fine for search results —
// they arrive as individual files with full paths. A browsed folder is neither: it's a path, and what the
// user means by clicking it is "everything under here". Expanding that into a file list needs the cache,
// so it happens HERE rather than in the browser, which would otherwise have to walk the tree one level
// request at a time and reconstruct paths it only half knows.
/**
* A folder can be enormous — the root of one measured peer is 284,166 files. Enqueueing that would be a
* misclick that takes the slskd instance down with it, so it's refused with the count rather than
* truncated to a silent partial download.
*/
export const MAX_ENQUEUE = 10_000;
export class DownloadError extends Error {
constructor(
message: string,
readonly status: number,
) {
super(message);
}
}
type EnqueueParams = { userId: number; username: string; path: string; file?: string };
/** Queue one file, or every file beneath one folder, from the cached tree. Returns how many were sent. */
export async function enqueueFromCache({ userId, username, path, file }: EnqueueParams): Promise<number> {
const base = getSlskdBase();
if (!base) throw new DownloadError('slskd upstream not configured', 503);
const files = await getSoulseekBrowseDownload({ userId, username, path, file });
if (!files.length) throw new DownloadError('nothing to download at that path', 404);
if (files.length > MAX_ENQUEUE) {
throw new DownloadError(
`${files.length.toLocaleString()} files is over the ${MAX_ENQUEUE.toLocaleString()} limit`,
413,
);
}
const headers = new Headers({ 'content-type': 'application/json' });
const apiKey = getSlskdApiKey();
if (apiKey) headers.set('X-API-Key', apiKey);
const res = await fetch(`${base}/api/v0/transfers/downloads/${encodeURIComponent(username)}`, {
method: 'POST',
headers,
body: JSON.stringify(files),
});
if (!res.ok) {
const detail = (await res.text().catch(() => '')).slice(0, 200);
throw new DownloadError(`slskd returned ${res.status}${detail ? `: ${detail}` : ''}`, 502);
}
console.log(`[slskd] queued ${files.length} file(s) from ${username}: ${file ?? path}`);
return files.length;
}