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
+2
View File
@@ -113,9 +113,11 @@ export {
getSoulseekBrowseLevel,
searchSoulseekBrowseTree,
getSoulseekBrowseDirFiles,
getSoulseekBrowseDownload,
deleteSoulseekBrowse,
} from './queries/soulseek';
export type {
BrowseDownloadFile,
BrowsedFile,
BrowseDirInput,
BrowseDirRow,
@@ -343,6 +343,47 @@ export async function getSoulseekBrowseDirFiles({
return row ? (row.files as BrowsedFile[]) : null;
}
/** A file as slskd wants it enqueued: the peer's own full path, plus the size it advertised. */
export type BrowseDownloadFile = { filename: string; size: number };
type BrowseDownloadParams = { userId: number; username: string; path: string; file?: string };
/**
* Resolve a download request to the exact files to enqueue — one folder's subtree, or a single file.
*
* Sizes and paths come from the cache rather than the client: slskd matches a queued download on
* filename AND size, so a wrong number is a transfer that never starts. The stored file name is a
* basename (browse, unlike search, doesn't repeat the folder), so the peer's real path is rejoined here.
*/
export async function getSoulseekBrowseDownload({
userId,
username,
path,
file,
}: BrowseDownloadParams): Promise<BrowseDownloadFile[]> {
const snapshotId = await snapshotIdOf(userId, username);
if (snapshotId === null) return [];
const rows = await db
.select({ name: soulseekBrowseDirs.name, files: soulseekBrowseDirs.files })
.from(soulseekBrowseDirs)
.where(
and(
eq(soulseekBrowseDirs.snapshotId, snapshotId),
// starts_with, not LIKE: these paths are full of backslashes, and one would be a LIKE escape.
file
? eq(soulseekBrowseDirs.name, path)
: sql`(${soulseekBrowseDirs.name} = ${path} or starts_with(${soulseekBrowseDirs.name}, ${path + SEP}))`,
),
)
.orderBy(asc(soulseekBrowseDirs.name));
return rows.flatMap((row) =>
(row.files as BrowsedFile[])
.filter((f) => (file ? f.name === file : true))
.map((f) => ({ filename: `${row.name}${SEP}${f.name}`, size: f.size })),
);
}
/** Drop a peer's cache entirely (snapshot + folders, via the FK cascade). */
export async function deleteSoulseekBrowse(userId: number, username: string): Promise<void> {
await db