config.toml reference
Every option Moosic reads from config.toml. Click the
gear icon in Moosic to open the file in your editor, or find it at the
path Moosic prints to its log on startup.
Top-level
Plain keys at the root of the file.
scale number default: 1.0UI scale multiplier. Use 1.5 or 2.0 on HiDPI screens that the OS doesn’t scale automatically.
scale = 1.5skin_path string (path) default: unset · procedural skinAbsolute path to a .wsz skin file. On Windows use forward slashes.
skin_path = "/Users/you/skins/base-2.91.wsz"skip_seconds number default: 15.0How far the transport skip buttons jump in either direction.
skip_seconds = 30.0art_cache_size integer default: 64Max album cover art images held in memory. Raise on RAM-rich machines with huge libraries; lower on small devices.
art_cache_size = 128scrobble_enabled boolean default: trueGlobal default for sending Subsonic scrobbles. Each server can override this.
scrobble_enabled = falsescrobble_threshold_percent integer 1–100 default: 50Percent of song duration that must play before the scrobble is submitted.
scrobble_threshold_percent = 75license string default: unset · trial modeSigned license string from your purchase email. Removes trial friction and unlocks premium features.
license = "eyJ...sig"[palette]
Modern UI color palette. Pick a preset and override any individual color. Colors are hex strings (#rrggbb or #rrggbbaa).
preset string default: "dark"Base palette. Valid: "dark", "light", "high_contrast". Any color set below this overrides the preset.
preset = "light"bg hex color default: preset bgWindow background.
bg = "#0b0a08"surface hex color default: preset surfacePanel / card background.
surface = "#141210"surface_high hex color default: presetElevated surface (hover, popovers).
surface_low hex color default: presetRecessed surface (inset wells).
inset hex color default: presetTrack grooves, sliders.
primary hex color default: preset accentAccent color used for highlights and active state.
primary = "#c8ff3a"primary_dim hex color default: presetFaded accent for backgrounds and ghost states.
tertiary hex color default: presetSecondary accent (warnings, badges).
on_surface hex color default: presetPrimary text color on surfaces.
on_surface_dim hex color default: presetSecondary / dimmed text color.
outline hex color default: presetDividers and borders.
[tabs]
Show or hide modern-UI tabs. Everything defaults to shown; Player and Library are always visible. Hiding a tab only hides the UI; configured podcast feeds and device syncing keep working in the background. Reloads on Eject; if the active tab disappears, the app falls back to Player.
podcasts boolean default: trueShow the PODS tab. Set false if you never use podcasts.
podcasts = falsedevices boolean default: trueShow the DEVICES tab (USB DAP / iPod sync).
devices = falsefriends boolean default: trueShow the FRIENDS (Fugue) / PARTY tab.
friends = false[[servers]]
One block per Subsonic-compatible server (Navidrome, Fugue, etc.). Add as many as you want; Moosic shows a server picker. Give several servers the same merge_group and they show up as one merged library (license feature).
name string default: requiredDisplay name in the server picker.
name = "Home Navidrome"url string (URL) default: required (unless ticket)Base URL of the server. Include scheme and port. Leave empty when using an Iroh ticket.
url = "http://192.168.1.42:4533"username string default: requiredAccount username on the server.
password string default: requiredAccount password. Stored in plain text in the file; protect with file permissions.
ticket string default: unsetIroh P2P ticket for connecting via QUIC instead of HTTP. Format: "FriendName:base64ticket". When set, url is ignored and traffic goes peer-to-peer.
ticket = "Anna:eyJp...dfQ"headers table (string → string) default: unsetCustom HTTP headers sent with every request to this server: API calls, streaming, cover art, event polling, device sync. For servers behind an auth proxy: Cloudflare Access service tokens, Authelia forward-auth headers, and the like. Plain Tailscale needs none of this; HTTP over the tailnet just works. Ignored for Iroh ticket connections (they bypass HTTP). Misconfigured header names produce a clear error at connect time. The inline form shown here is safest; a separate [servers.headers] block also works but binds to the most recent [[servers]] entry above it.
headers = { CF-Access-Client-Id = "xxx.access", CF-Access-Client-Secret = "yyy" }client_cert string (path) default: unsetPEM file with an mTLS client certificate presented to this server. May include the private key inline; otherwise set client_key too. ~/ expands. Unreadable cert files produce a clear error at connect time.
client_cert = "~/.config/moosic/client.pem"client_key string (path) default: unsetPEM file with the private key for client_cert, when kept separate from the certificate. ~/ expands.
client_key = "~/.config/moosic/client.key"ca_cert string (path) default: unsetPEM file with a private CA root to trust for this server: self-hosted setups with their own CA. ~/ expands.
ca_cert = "~/.config/moosic/internal-ca.pem"scrobble_enabled boolean default: inherit globalOverride the top-level scrobble_enabled for this server only.
scrobble_threshold_percent integer 1–100 default: inherit globalOverride the top-level threshold for this server only.
merge_group string default: unsetMerge this server into a named group. All servers with the same merge_group appear as ONE combined library: artists, albums, search and playlists from every member, duplicates collapsed to the best-quality copy (bitrate × format). Scrobbles and per-server settings still go to the track's own server. Requires a license with the multi_server feature; without one, the servers stay individual entries in the picker.
merge_group = "Everything"[[servers.backends]]
Bring-your-own-library on a multi-user Fugue server. Declare Navidrome backends under a [[servers]] entry; Moosic registers them on that Fugue when it connects (matched by name, so restarts never duplicate) and applies your sharing choices. Ignored for plain Navidrome servers. Nesting decides the target: each [[servers.backends]] block belongs to the [[servers]] entry above it.
name string default: requiredBackend name on the Fugue server. Used to match on reconnect; rename creates a new backend.
name = "my-navidrome"url string (URL) default: requiredBase URL of your Navidrome/Subsonic server. Fugue connects to it server-side; other users never see this URL or your credentials.
url = "https://music.example.com"username string default: requiredYour account on that backend.
password string default: requiredPassword for the backend account. Sent once to Fugue over your authenticated connection; stored encrypted at rest on the server.
share string default: "private"Who may browse this backend on the Fugue server. "private" = only you. "server" = every user on this Fugue can opt in. "federated" = also linked Fugue servers.
share = "server"share_with array of strings default: []Usernames to share with individually, on top of `share`. Works with share = "private" for share-with-only-Alice setups.
share_with = ["alice", "bob"][podcasts]
RSS podcast subscriptions for the Podcasts tab. Feeds added through the UI are stored separately (podcasts.json in the data dir) and merged with these. Connected to a Fugue server with native podcasts? The library moves server-side: config feeds and local listened-state are pushed once, then subscriptions and episode positions sync across all your devices and Fugue does the streaming. On plain Navidrome everything stays local to the device.
feeds array of strings (URLs) default: []RSS/Atom feed URLs to subscribe to. These are read-only from the UI; remove them here. Feeds added via the + button in the app land in podcasts.json instead.
feeds = [
"https://atp.fm/rss",
]auto_download integer default: 0Download the N newest unplayed episodes per show for offline playback (0 = off). Downloads always come straight from the feed’s enclosure URL, also in Fugue mode; device sync needs local files.
auto_download = 2sync_newest_unplayed integer default: 0Copy the N newest unplayed (and downloaded) episodes per show to connected devices, iPods and other players (0 = off). Implies downloading at least N episodes.
sync_newest_unplayed = 3remove_played_from_device boolean default: truePrune episodes you have finished from the device on the next sync, keeping the player topped up with fresh ones.
remove_played_from_device = truerefresh_minutes integer default: 60How often feeds refresh while the app runs (minimum 5). In Fugue mode the server refreshes feeds on its own schedule too.
refresh_minutes = 30[sink]
Where decoded audio is sent. Defaults to local speakers (cpal). For Snapcast streaming, write to a FIFO pipe.
kind string default: "cpal"Audio sink kind. "cpal" = local speakers. "snapcast" = stream to a Snapcast server via FIFO.
kind = "snapcast"snapcast.pipe string (path) default: required for snapcastPath to a FIFO created with mkfifo. Snapserver must read from the same pipe.
pipe = "/tmp/snapfifo"snapcast.sample_rate integer Hz default: 48000Sample rate written to the pipe. Must match the snapserver source config.
snapcast.channels integer default: 2Channel count written to the pipe.
[[sync_folders]]
Folder destinations shown alongside auto-detected USB devices in the Devices tab. Use for SD cards, NAS shares, dev simulators.
name string default: requiredDisplay name in the Devices tab.
name = "Backup SD card"path string (path) default: requiredDestination folder. Tracks go under <path>/Music/<Artist>/<Album>/, playlists under <path>/Playlists/.
path = "/Volumes/MUSIC"profile string default: auto-detectDevice profile id ("snowsky_echo", "generic_msc", or a custom id). When omitted, the registry picks the best match.
profile = "snowsky_echo"encoding.mode string default: "auto"Per-folder transcode policy. "auto" = only transcode formats the device can't play. "always" = force the target codec even for playable files (fit more music). "never" = copy as-is, skip unplayable tracks. Overrides the profile default.
[sync_folders.encoding]
mode = "always"encoding.codec string default: best acceptedTarget codec when transcoding: flac, alac, aac, mp3, opus, ogg, wav. Omit to let Moosic pick the best codec the device accepts.
codec = "aac"encoding.max_bitrate_kbps integer default: encoder defaultLossy bitrate ceiling in kbps (e.g. 256). Ignored for lossless codecs (flac/alac/wav).
max_bitrate_kbps = 256[[device_override]]
Encoding override applied to an auto-detected USB/mounted device by its profile id, without editing the profile. Folders carry encoding inline instead; this is for real plugged-in players.
profile string default: requiredProfile id this override targets ("snowsky_echo", "ipod", "generic_msc", or a custom id).
profile = "ipod"mode string default: "auto"Transcode policy: "auto", "always", or "never". Same meaning as sync_folders.encoding.mode.
mode = "always"codec string default: best acceptedTarget codec: flac, alac, aac, mp3, opus, ogg, wav.
codec = "aac"max_bitrate_kbps integer default: encoder defaultLossy bitrate ceiling in kbps. Ignored for lossless codecs.
max_bitrate_kbps = 256[[device_group]]
A named group of devices that share one marked sync set: mark a playlist once and it syncs to every device in the group. Defined in config (no in-app UI yet); edit membership here.
name string default: requiredGroup display name.
name = "Cardio gear"devices array of strings default: []Connected-device ids in the group. Folder destinations are "folder:<name>"; mounted volumes are "mount:<path>".
devices = ["folder:Phone", "mount:/Volumes/MUSIC"][[device_profile]]
Custom DAP profile defined in config alone, no rebuild. Use when a device is close to the generic layout and only the folder names, codec list, or USB IDs differ.
id string default: requiredUnique profile id. Reference this from sync_folders.profile or USB auto-detect.
id = "my_weird_dap"display_name string default: requiredHuman-readable name shown in the Devices tab.
vendor string default: ""Vendor name. Cosmetic.
usb array of {vid,pid} default: []USB vendor/product IDs that should auto-match this profile.
usb = [{ vid = 0x1234, pid = 0x5678 }]mount_markers array of strings default: []Filenames that confirm the device when found at a mount root.
mount_markers = [".mydap"]volume_label_hints array of strings default: []Volume labels (case-insensitive) that should auto-match.
music_root string default: "Music"Folder under the mount root where tracks are written.
playlist_root string default: "Playlists"Folder under the mount root where playlists are written.
playlist_format string default: "m3u8"Playlist file format. One of: "m3u8", "m3u", "wpl", "pls".
path_template string default: "{album_artist}/{album}/{track:02} {title}.{ext}"Per-track path template. Placeholders: {album_artist} {artist} {album} {track} {disc} {title} {ext}.
path_max_len integer default: 240Truncate paths that exceed this many bytes (FAT32 limits).
case_sensitive boolean default: falseWhether path comparison is case-sensitive on this device.
forbidden_chars string default: \/:*?"<>|Characters stripped from generated paths.
artwork string default: "embedded"Artwork strategy. One of: "embedded", "sidecar_folder", "sidecar_match", "none".
codecs array of strings default: ["flac","mp3","aac","ogg","wav"]Codecs the device can decode. Other formats are transcoded before copy.
max_sample_rate integer Hz default: 192000Max sample rate. Files above this are downsampled.
max_bit_depth integer default: 24Max bit depth.
max_channels integer default: 2Max channel count.
supports_dsd boolean default: falseWhether the device plays DSD natively.
post_sync_marker string default: unsetFilename written under the mount root after a successful sync. Triggers a library rescan on devices that watch for one.
post_sync_marker = ".rescan"encode_mode string default: "auto"Default transcode policy for this profile: "auto", "always", or "never". A [[device_override]] or per-folder encoding still wins.
encode_mode = "always"encode_codec string default: best acceptedDefault transcode target codec: flac, alac, aac, mp3, opus, ogg, wav.
encode_codec = "aac"encode_max_bitrate_kbps integer default: encoder defaultDefault lossy bitrate ceiling in kbps. Ignored for lossless codecs.
encode_max_bitrate_kbps = 256Full example
Every option set at once. Copy, then delete what you don't need.
Show complete config.toml
# ─── Top-level ────────────────────────────────────────────────────────
scale = 1.0
skin_path = "/Users/you/skins/base-2.91.wsz"
skip_seconds = 15.0
art_cache_size = 64
scrobble_enabled = true
scrobble_threshold_percent = 50
license = "eyJ...sig"
# ─── Modern UI palette ────────────────────────────────────────────────
[palette]
preset = "dark"
primary = "#c8ff3a"
bg = "#0b0a08"
surface = "#141210"
# ─── Servers ──────────────────────────────────────────────────────────
[[servers]]
name = "Home Navidrome"
url = "http://192.168.1.42:4533"
username = "anna"
password = "navidrome-password"
# Per-server overrides:
# scrobble_enabled = false
# scrobble_threshold_percent = 75
# Merge into one library with every server sharing this group name
# (requires the multi_server license feature):
merge_group = "Everything"
[[servers]]
name = "Remote Navidrome"
url = "https://music.example.com"
username = "anna"
password = "other-password"
merge_group = "Everything"
# Behind an auth proxy (Cloudflare Access, Authelia, …) or mTLS?
# Headers and cert paths are per server; ~/ expands:
# headers = { CF-Access-Client-Id = "xxx.access", CF-Access-Client-Secret = "yyy" }
# client_cert = "~/.config/moosic/client.pem"
# ca_cert = "~/.config/moosic/internal-ca.pem"
# Fugue over Iroh P2P (url ignored when ticket is set):
[[servers]]
name = "Friend's Fugue"
url = ""
username = "guest"
password = "shared-pw"
ticket = "Anna:eyJp...dfQ"
# Bring your own library to that Fugue: registered on connect,
# shared with everyone on the server (they still opt in per device).
[[servers.backends]]
name = "my-navidrome"
url = "https://music.example.com"
username = "me"
password = "navidrome-password"
share = "server"
# share_with = ["alice", "bob"]
# ─── Tabs ─────────────────────────────────────────────────────────────
# Hide modern-UI tabs you never use (all default to shown).
# Player and Library are always visible.
[tabs]
podcasts = false
# devices = false
# friends = false
# ─── Podcasts ─────────────────────────────────────────────────────────
# Feeds added in the app live in podcasts.json; these are merged in.
# On a Fugue server with native podcasts the library syncs across devices.
[podcasts]
feeds = [
"https://atp.fm/rss",
]
auto_download = 2 # download N newest unplayed per show (0 = off)
sync_newest_unplayed = 3 # copy N newest unplayed per show to devices (0 = off)
remove_played_from_device = true
refresh_minutes = 60
# ─── Audio sink ───────────────────────────────────────────────────────
# Default is local speakers. To stream to Snapcast via FIFO:
# mkfifo /tmp/snapfifo
[sink]
kind = "snapcast"
[sink.snapcast]
pipe = "/tmp/snapfifo"
sample_rate = 48000
channels = 2
# ─── Device sync ──────────────────────────────────────────────────────
[[sync_folders]]
name = "Dev simulator"
path = "/Users/you/dev/fake-echo"
profile = "snowsky_echo"
[[sync_folders]]
name = "Backup SD card"
path = "/Volumes/MUSIC"
# Force everything to 256 kbps AAC to fit more music on the card:
[sync_folders.encoding]
mode = "always"
codec = "aac"
max_bitrate_kbps = 256
# ─── Per-device encoding override ─────────────────────────────────────
# Force transcode on an auto-detected player by its profile id.
[[device_override]]
profile = "ipod"
mode = "always"
codec = "aac"
max_bitrate_kbps = 256
# ─── Device groups ────────────────────────────────────────────────────
# Mark a playlist once → syncs to every device in the group.
[[device_group]]
name = "Cardio gear"
devices = ["folder:Phone", "mount:/Volumes/MUSIC"]
# ─── Custom DAP profile ───────────────────────────────────────────────
[[device_profile]]
id = "my_weird_dap"
display_name = "My Weird DAP"
vendor = "Acme"
usb = [{ vid = 0x1234, pid = 0x5678 }]
mount_markers = [".mydap"]
volume_label_hints = ["WEIRD"]
music_root = "Audio"
playlist_root = "Lists"
playlist_format = "m3u8"
path_template = "{album_artist}/{album}/{track:02} {title}.{ext}"
path_max_len = 240
case_sensitive = false
forbidden_chars = "\\/:*?\"<>|"
artwork = "embedded"
codecs = ["flac", "mp3", "ogg"]
max_sample_rate = 96000
max_bit_depth = 24
max_channels = 2
supports_dsd = false
post_sync_marker = ".rescan"
encode_mode = "auto"
encode_codec = "aac"
encode_max_bitrate_kbps = 256