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.
57 lines
2.3 KiB
TypeScript
57 lines
2.3 KiB
TypeScript
import { getSoulseekBrowseDownload } from '../db';
|
|
import { authHeaders, getSlskdConfig } 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 cfg = await getSlskdConfig(userId);
|
|
if (!cfg) 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 res = await fetch(`${cfg.base}/api/v0/transfers/downloads/${encodeURIComponent(username)}`, {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json', ...authHeaders(cfg) },
|
|
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;
|
|
}
|