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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user