{#snippet sw(on, words, onchange, disabled = false)} {on ? words[0] : words[1]} {/snippet} {#snippet seg(options, value, onpick, disabled = false)} {#each options as [v, label] (v)} {/each} {/snippet}
{#if !settings}

Loading…

{:else}

Account

Your Soulseek login and where the password lives.
Soulseek account {#if settings.connected || online}online{:else if settings.username}offline{/if}
{settings.username ? `Signed in as ${settings.username}.` : "Not signed in."} Your password is stored in the Windows Credential Manager, not in a plain file.
{#if app.connection !== "online"} {/if} {#if settings.has_password} {/if}
First-time setup
The welcome tour from the first launch: library folder, account, community peer, look. Runs again from here and changes nothing until you do.

Networks

The two networks this app speaks, and whether they are tied together.
Soulseek {#if online}online{:else}offline{/if}{#if ownLinkCurrent}linked{/if}
{#if me}Signed in as {me} · {fmtCount(network.shareFiles)} files shared.{:else}Not signed in.{/if} {#if !key} Create a SoulStream identity on Community → You to link this account to it. {:else if !me} Sign in to Soulseek to link this account to your identity. {:else if ownLinkCurrent} Linked to your SoulStream identity — proof verified {new Date(ownLink.verified_at * 1000).toLocaleString()} through a message to yourself. Other SoulStream users who ask get a signed answer. {:else} Not linked to your identity. Linking sends one message to yourself over Soulseek and verifies the signed answer — nothing is published. {/if}
{#if key && me} {#if ownLinkCurrent} {:else} {/if} {/if}
Freenet experimental
{#if freenetOn} Local node on 127.0.0.1:{community.freenet?.ws_port ?? 7509} · {status.text} {:else} Off. Carries your identity, profile, playlists, friend links, Spaces and holdings. Turning it on publishes nothing; turning it off is instant and touches nothing else in the app. {/if}
{#if freenetOn && statusKind(community.freenet?.status) === "node-absent"}
{#if community.nodeInfo?.binary} runs the installed peer{#if community.nodeInfo.version} ({community.nodeInfo.version}){/if} in the background · telemetry pinned off · data in the app folder {:else} No Freenet peer found on this machine — install it (cargo install freenet) and check again. {/if}
{:else if freenetOn && community.nodeInfo?.started_by_app}
started by SoulStream · keeps running when the app closes; this button stops it
{/if}
Before you rely on it — seven things that are true
  • A desktop background peer must run; it uses network, disk, memory and CPU.
  • The network is alpha. Things you publish are replicated and cannot be guaranteed erased.
  • Freenet is not an anonymity service.
  • The node's own diagnostics are a node setting SoulStream pins off; verify after upgrades.
  • Nothing is published when you enable this. Publishing is always a button.
  • Nothing here ever says "online" — a successful fetch is not presence.
  • Phones and TVs cannot run the same peer today.
{@render sw(freenetOn, ["on", "off"], (v) => setFreenetEnabled(v), !!community.busy.freenet)}
{#if freenetOn}
Load remote profile images
Other people's avatars, fetched from the network — verified, resized, cached by hash. Off shows their identicon only.
{@render sw(community.freenet?.load_remote_images ?? true, ["on", "off"], (v) => setLoadRemoteImages(v), !!community.busy.images)}
{#if key}
Encrypted inbox {#if community.inbox?.published} key {community.inbox.box_key_id}{/if}
{#if community.inbox?.published}Friend requests and shares arrive sealed to a key kept on this device{#if community.inbox.last_check} · checked {fmtWhen(community.inbox.last_check * 1000)}{/if}.{:else}Lets other SoulStream users send you friend requests and shares sealed, without Soulseek in between.{/if}
{#if community.inbox?.published} {:else} {/if}
{/if} {/if} {#if community.error}
{community.error}
{/if}

Updates

New releases from the project, on the channel you pick. Installing is always a button.
SoulStream {up?.version ?? "…"} {#if up?.dev_build}dev build{/if}{#if avail}{avail.version} available{:else if up?.last_check && !up?.last_error}up to date{/if}
{#if up?.checking}Checking {up.channel}… {:else if up?.last_error}Last check failed: {up.last_error} {:else if up?.last_check}Checked {fmtWhen(up.last_check * 1000)} on the {up.channel} channel. {:else}Not checked yet on the {up?.channel ?? "stable"} channel.{/if} {#if updater.error} {updater.error}{/if}
{#if avail}
{avail.version} is ready {#if avail.date}{new Date(avail.date * 1000).toLocaleDateString()}{/if}
{#if avail.notes}
{avail.notes}
{/if} {#if up?.installing}
{pct != null ? `Downloading · ${pct}%` : "Downloading…"} — the app closes into the installer and comes back on its own.
{:else if up?.dev_build}
This is a dev build run from the target folder; the installer would set up the packaged app beside it.
{/if}
{/if}
Channel
Stable follows the releases. Nightly is built from main every day and can break; switching back to Stable offers the next stable build even when its number is lower.
{@render seg([["stable", "Stable"], ["nightly", "Nightly"]], up?.channel ?? "stable", (v) => setUpdater({ channel: v }), !up || !!up.installing)}
Check automatically
Shortly after launch and every six hours. Finding one shows a toast; nothing installs on its own.
{@render sw(up?.auto_check ?? true, ["on", "off"], (v) => setUpdater({ autoCheck: v }), !up)}
Access token {#if up?.has_token}set{:else if up?.source_kind === "github"}needed for a private repository{/if}
The project's repository is private while the alpha runs: releases are read with a token that can only read them. Paste the one you were given — it goes to the Windows Credential Manager, not a plain file, and leaves with Clear.
e.key === "Enter" && saveToken()} disabled={!up} /> {#if up?.has_token}{/if}
Update source
The manifest each channel reads (latest.json, Tauri's updater format, signed with the project's key). Leave empty for the project's GitHub releases; point it at a mirror or your own Gitea to update from there.

Appearance

How the window is drawn.
Dark glass
Lets whatever is behind the window glow through the app's dark surfaces with a live blur (Windows acrylic). Cards, rows and highlights thin out with it; menus and dialogs stay solid so nothing gets hard to read.
{@render sw(!!settings.glass_mode, ["dark glass", "solid"], (v) => setGlassMode(v), saving)}

Playback

Output and loudness.
Audio output
System default follows Windows when headphones or speakers change.
setAudioOutput(v)} disabled={saving} />
Normalize volume
Plays every track at roughly the same loudness (−14 LUFS, like Spotify), so quiet and loud masters stop needing the volume knob. A track is measured while it first streams and matched exactly from then on; until that finishes SoulStream will turn it down but never up, so a quiet intro can't make the chorus too loud.
{@render sw(!!settings.volume_normalization, ["on", "off"], (v) => setVolumeNormalization(v), saving)}
Changes apply immediately and keep the current track near the same position.

Lyrics

Line sync is always on; word fill is the experiment.
Word-by-word fill experimental
Fills each word as it is sung, karaoke-style. Still experimental: timing can drift on dense or echo-heavy songs. Off keeps the reliable line-by-line sync everywhere and pauses the background word analysis.
{@render sw(!!settings.word_timing, ["on", "off"], (v) => setWordTiming(v), saving)}
{#if settings.word_timing}
Precise word timing
Downloads an alignment model (MMS, ~1.26 GB, from Hugging Face) that listens to each lyric line and times every word against the actual singing, in most languages. Runs entirely on this computer, in the background, never against the playing track. Without it, word timing stays a lighter estimate. Already-timed songs re-prepare automatically the next time you open their lyrics.
{#if modelError}
Download failed: {modelError}
{/if}
{#if lyricModel?.present} Downloaded {:else} {/if}
{#if lyricModel?.present}
Vocal isolation
Downloads a small separation model (UVR MDX-Net Voc FT, ~64 MB, from the Ultimate Vocal Remover project) that lifts the singing away from the band before words are timed — noticeably steadier timing on dense mixes. Same rules as above: runs on this computer, in the background, never against the playing track.
{#if sepError}
Download failed: {sepError}
{/if}
{#if sepModel?.present} Downloaded {:else} {/if}
{/if} {/if}

Streaming

What happens to a track after you streamed it.
Keep streamed tracks
Streamed songs stay in a private cache for instant replay — not in your library. Forever adds everything you stream to the library instead.
setStreamRetention(v)} disabled={saving} />
Cache size limit
Oldest streamed tracks are removed first once the cache passes this size. Saved and downloaded music is never touched.
setStreamCacheMax(v)} disabled={saving} />
Retention counts from the last time a track was played. Tracks you save stay in your library regardless.

Library & downloads

Where music lands and how it is filed.
Library folder
{settings.download_dir}
Your downloaded-music library, always shared. Folder changes apply after reconnecting.
Album folder format
Use {"{artist}"}, {"{album}"}, {"{date}"} or {"{year}"}; "\" starts a subfolder. Full dates display as DD-MM-YYYY. New downloads are organized into this format; existing files are not moved.
e.key === "Enter" && e.currentTarget.blur()} onchange={(e) => setLibraryLayout(e.currentTarget.value)} />

Sharing

Sharing keeps you a good citizen — leechers get banned.
Shared folders
Your library folder is always shared ({fmtCount(settings.share_files)} files indexed). Add extra folders here.
{#if settings.shared_folders.length > 0}
{#each settings.shared_folders as folder (folder)}
{folder}
{/each}
{/if}
Upload slots
How many peers can download from you at once.
{settings.upload_slots}

Source quality

Independent preferences for streaming and downloading.
The ceiling and floor bound which peer files qualify; priority decides whether the resolver picks the best format or the fastest transfer among them. Fastest estimates arrival from file size ÷ peer speed plus queue wait — a large FLAC from a fast peer can win.
{#each [{ key: "stream", label: "Streaming" }, { key: "download", label: "Downloads" }] as mode (mode.key)}
{mode.label}
Maximum
Skip files above this ceiling — lossy-only avoids huge lossless transfers.
{@render seg([["any", "No limit"], ["lossy_only", "Lossy only"]], sourcePrefsFor(mode.key).max, (v) => setSourcePref(mode.key, "max", v), saving)}
Minimum
Reject files below this floor. Lossy files must report a bitrate to qualify.
setSourcePref(mode.key, "min", v)} disabled={saving} />
Priority
Only files inside the window above compete.
{@render seg([["quality", "Best quality"], ["speed", "Fastest transfer"]], sourcePrefsFor(mode.key).priority, (v) => setSourcePref(mode.key, "priority", v), saving)}
{/each}

Album artwork

Covers come from Deezer, Apple and the Cover Art Archive; the catalog's wins, at the highest resolution any source serves.
Art inside downloaded files
Replace whatever picture the uploader embedded with the canonical cover, or keep theirs when present.
{@render seg([["replace", "Replace with canonical"], ["keep", "Keep uploader's art"]], settings.replace_embedded_art ? "replace" : "keep", (v) => setReplaceEmbeddedArt(v), saving)}
Embedded cover size
Longest edge of the copy written into each audio file. The full-resolution original is always kept in the album folder and art cache.
{@render seg([["0", "Original"], ["1500", "1500 px"], ["1000", "1000 px"]], String(settings.embed_art_max_edge), (v) => setEmbedArtMaxEdge(v), saving)}
Album folder cover
Write the canonical cover as cover.jpg and folder.jpg so Explorer and other players show the same artwork.
{@render sw(!!settings.write_folder_cover, ["write", "leave"], (v) => setWriteFolderCover(v ? "write" : "skip"), saving)}
Re-fetch all album covers
Upgrade every library album to the canonical high-resolution cover — cache, embedded tags and folder files. Roughly one album per second.
{#if albumRefreshResult}
{albumRefreshResult}
{/if}

Artist artwork

Portraits and the hero renderer.
Hero renderer
Live GPU creates one native-4K master (crop → EASU → RCAS → color/fade/grain once at 3840×2160), then mip-filters it cleanly for every window size, preserving grain scale.
{@render seg([["live_gpu", "Live GPU 4K"], ["fsr_legacy", "Baked FSR 1"], ["lanczos3", "Baked Lanczos3"]], settings.artist_hero_upscaler ?? "live_gpu", (v) => setArtistHeroUpscaler(v), saving)}
Refresh artist portraits
Fetch portraits for artists already in your library.
{#if artistRefreshResult}
{artistRefreshResult}
{/if}
{/if}