Let providers be enabled/disabled via crabidy-server.toml

The server now writes a default crabidy-server.toml on first start listing
every provider:

    providers = ["tidal", "youtube", "fs", "crabidy", "orphans"]

Removing a name disables that provider — it no longer mounts and drops out
of the library; its own config file is left unread. An absent providers key
(a deleted line, or a fresh install with no file) enables all of them, so a
server never silently loses its whole library. Disabling crabidy also drops
orphans, which is a view over the store.

ServerSettings gains the providers list, provider_enabled/provider_toggles,
and ensure_default (best-effort first-run seed). ProviderOrchestrator::init
becomes ::build(ProviderToggles), gating each provider; the tidal client is
now Option like the others (still fatal-on-error when enabled, skipped when
disabled). README and the mdbook document the list.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Test User 2026-07-23 00:27:02 +02:00
parent 3a03114cb9
commit d078e65a7d
5 changed files with 355 additions and 94 deletions

View File

@ -57,7 +57,7 @@ filled in.
| `fsdy.toml` | local fs | [fsdy/README.md](fsdy/README.md) |
| `cbd-tui.toml` | `cbd-tui` | below |
| `cbd.toml` | `cbd` | below (same options as `cbd-tui.toml`) |
| `crabidy-server.toml`| server | below (never auto-created) |
| `crabidy-server.toml`| server | below (providers + auth) |
The server-managed `crabidy` provider does not live under `~/.config`. Its
track-file tree (saved queues, bookmarks, and captures) lives in
@ -105,7 +105,21 @@ cbd-tui auth owner 'my-password' # sets user + password
cbd-tui auth queue-owner 'pw' --address http://pi:50051
```
### `crabidy-server.toml` — roles and rights
### `crabidy-server.toml` — providers and rights
On first start the server writes this file with every provider enabled:
```toml
providers = ["tidal", "youtube", "fs", "crabidy", "orphans"]
```
**Remove a name to disable that provider** — it no longer mounts and does
not appear in the library. Deleting the whole `providers` line re-enables
everything (a fresh install with no file behaves the same). Disabling
`crabidy` also drops `orphans`, which is a view over the store. A disabled
provider's own config file (`tidaly.toml`, etc.) is simply left unread.
#### Roles and rights
By default the server is open: everyone who can reach the port has
full control. Adding an `[auth]` section turns on HTTP basic auth for

View File

@ -17,7 +17,7 @@ use crabidy_core::proto::crabidy::{
crabidy_service_server::CrabidyServiceServer, InitResponse, LibraryNode, PlayState, Queue,
Track,
};
use crabidy_core::{ProviderClient, ProviderError};
use crabidy_core::ProviderError;
use rand::{rng, seq::SliceRandom};
use std::sync::{atomic::AtomicBool, Arc};
use std::time::SystemTime;
@ -45,14 +45,21 @@ pub async fn serve(
let config_dir = dirs::config_dir()
.map(|d| d.join("crabidy"))
.unwrap_or_else(|| std::path::PathBuf::from("/tmp"));
// Seed a default config (all providers, no auth) on first run so users
// have the full provider list to prune. Best-effort — a write failure just
// means we fall back to the all-enabled default below.
if let Err(err) = settings::ServerSettings::ensure_default(&config_dir) {
warn!("could not write a default crabidy-server.toml: {err}");
}
let server_settings = settings::ServerSettings::load(&config_dir)?;
let authenticator = Arc::new(auth::Authenticator::new(&server_settings.auth));
if authenticator.enabled() {
info!("role authorization enabled");
}
let toggles = server_settings.provider_toggles();
let (update_tx, _) = tokio::sync::broadcast::channel(2048);
let orchestrator = provider::ProviderOrchestrator::init("")
let orchestrator = provider::ProviderOrchestrator::build(toggles)
.await
.map_err(|err| {
error!("failed to init provider orchestrator: {err}");

View File

@ -1,5 +1,6 @@
use crate::crabidy_store::{CrabidyStore, SaveMode, CRABIDY_PROVIDER_ROOT, CURRENT_NAME};
use crate::orphans::{OrphansProvider, ORPHANS_PROVIDER_ROOT};
use crate::settings::ProviderToggles;
use crate::{ProviderCommand, ProviderMessage};
use async_trait::async_trait;
use crabidy_core::{
@ -13,7 +14,10 @@ use tracing::{debug, debug_span, error, instrument, warn, Instrument};
pub struct ProviderOrchestrator {
pub provider_tx: flume::Sender<ProviderMessage>,
provider_rx: flume::Receiver<ProviderMessage>,
tidal_client: Arc<tidaldy::Client>,
/// The Tidal provider; `None` when disabled in `crabidy-server.toml` (or,
/// unlike the other providers, when its config fails to load — that stays
/// fatal at startup, see [`ProviderOrchestrator::build`]).
tidal_client: Option<Arc<tidaldy::Client>>,
/// `None` when the filesystem provider failed to initialize — the
/// server runs without `/fs` instead of dying (architecture D5).
fs_client: Option<Arc<fsdy::Client>>,
@ -57,6 +61,15 @@ fn orphans_owns(path: &str) -> bool {
}
impl ProviderOrchestrator {
/// The tidal client, or `MalformedPath` (with a warning) when the
/// provider is disabled — a `/tidal` path then has no owner.
fn tidal_provider(&self) -> Result<&tidaldy::Client, ProviderError> {
self.tidal_client.as_deref().ok_or_else(|| {
warn!("tidal provider is disabled");
ProviderError::MalformedPath
})
}
/// The fs client, or `MalformedPath` (with a warning) when the
/// provider is disabled — a `/fs` path then has no owner.
fn fs_provider(&self) -> Result<&fsdy::Client, ProviderError> {
@ -224,10 +237,15 @@ impl ProviderOrchestrator {
}
}
#[async_trait]
impl ProviderClient for ProviderOrchestrator {
#[instrument(skip(_s))]
async fn init(_s: &str) -> Result<Self, ProviderError> {
impl ProviderOrchestrator {
/// Builds the orchestrator, mounting only the providers `enabled` turns on
/// (from `crabidy-server.toml`'s `providers` list). Tidal, when enabled,
/// still aborts startup on a broken config — its historical behavior — but
/// a disabled tidal skips it entirely; the other providers are non-fatal
/// and just drop their subtree. `/orphans` additionally needs the store,
/// so it only mounts when `crabidy` does.
#[instrument]
pub async fn build(enabled: ProviderToggles) -> Result<Self, ProviderError> {
let config_dir = dirs::config_dir()
.map(|d| d.join("crabidy"))
.unwrap_or(PathBuf::from("/tmp"));
@ -239,25 +257,32 @@ impl ProviderClient for ProviderOrchestrator {
.await
.map_err(|e| ProviderError::Config(e.to_string()))?;
}
// Tidal: skipped when disabled; when enabled a broken config is still
// fatal (unlike the local providers), preserving prior behavior.
let tidal_client = if enabled.tidal {
let config_file = config_dir.join("tidaly.toml");
debug!(config_file = %config_file.display(), "loading tidal config");
let raw_toml_settings = fs::read_to_string(&config_file).unwrap_or_default();
let tidal_client = Arc::new(tidaldy::Client::init(&raw_toml_settings).await.map_err(
|err| {
let client = tidaldy::Client::init(&raw_toml_settings)
.await
.map_err(|err| {
error!("failed to init tidal client: {err}");
err
},
)?);
let new_toml_config = tidal_client.settings();
if let Err(err) = tokio::fs::write(&config_file, new_toml_config).await {
})?;
if let Err(err) = tokio::fs::write(&config_file, client.settings()).await {
error!("failed to write tidal config file: {err}");
}
Some(Arc::new(client))
} else {
None
};
// The filesystem provider is optional: a broken local config only
// costs the `/fs` subtree, never the server.
let fs_client = if enabled.fs {
let fs_config_file = config_dir.join("fsdy.toml");
debug!(config_file = %fs_config_file.display(), "loading fs config");
let raw_fs_settings = fs::read_to_string(&fs_config_file).unwrap_or_default();
let fs_client = match fsdy::Client::init(&raw_fs_settings).await {
match fsdy::Client::init(&raw_fs_settings).await {
Ok(client) => {
if let Err(err) = tokio::fs::write(&fs_config_file, client.settings()).await {
error!("failed to write fsdy config file: {err}");
@ -268,13 +293,17 @@ impl ProviderClient for ProviderOrchestrator {
warn!("filesystem provider disabled: {err}");
None
}
}
} else {
None
};
// The single `/crabidy` provider: one content store + toml tree
// (architecture/crabidy-store.md). The store owns both roots (state
// tree + data store) and is the sole writer; the mounted `fsdy`
// client reads the tree and resolves store playables. Non-fatal like
// the other local providers — no state/data dir just drops `/crabidy`.
let crabidy_store = match (
let crabidy_store = if enabled.crabidy {
match (
CrabidyStore::default_tree_root(),
CrabidyStore::default_store_root(),
) {
@ -289,6 +318,9 @@ impl ProviderClient for ProviderOrchestrator {
warn!("crabidy library disabled: no state/data directory");
None
}
}
} else {
None
};
// Saved queues/bookmarks are renamable and deletable; the
// auto-persisted `current` stays untouchable. The whole tree is
@ -310,23 +342,29 @@ impl ProviderClient for ProviderOrchestrator {
}
}
});
// `/orphans`: a view over the store, mounted only when the store is.
// Its reference roots are the disk roots of the mounted file providers
// — the `/crabidy` toml tree and (when enabled) the `/fs` root — the
// only places a `Playable::Store` link can live (architecture/orphans.md).
let orphans_client = crabidy_store.as_ref().map(|store| {
// `/orphans`: a view over the store, so it needs both its own toggle
// and a live store. Its reference roots are the disk roots of the
// mounted file providers — the `/crabidy` toml tree and (when enabled)
// the `/fs` root — the only places a `Playable::Store` link can live
// (architecture/orphans.md).
let orphans_client = if enabled.orphans {
crabidy_store.as_ref().map(|store| {
let mut ref_roots = vec![store.tree_dir().to_path_buf()];
if let Some(fs) = &fs_client {
ref_roots.push(fs.disk_root().to_path_buf());
}
Arc::new(OrphansProvider::new(Arc::clone(store), ref_roots))
});
})
} else {
None
};
// YouTube: non-fatal like the local providers — a missing or
// broken yt-dlp binary only costs the `/youtube` subtree.
let youtube_client = if enabled.youtube {
let yt_config_file = config_dir.join("ytdy.toml");
debug!(config_file = %yt_config_file.display(), "loading youtube config");
let raw_yt_settings = fs::read_to_string(&yt_config_file).unwrap_or_default();
let youtube_client = match ytdy::Client::init(&raw_yt_settings).await {
match ytdy::Client::init(&raw_yt_settings).await {
Ok(client) => {
if let Err(err) = tokio::fs::write(&yt_config_file, client.settings()).await {
error!("failed to write ytdy config file: {err}");
@ -337,6 +375,9 @@ impl ProviderClient for ProviderOrchestrator {
warn!("youtube provider disabled: {err}");
None
}
}
} else {
None
};
let (provider_tx, provider_rx) = flume::bounded(100);
Ok(Self {
@ -350,6 +391,17 @@ impl ProviderClient for ProviderOrchestrator {
youtube_client,
})
}
}
#[async_trait]
impl ProviderClient for ProviderOrchestrator {
/// Builds with every provider enabled. The server calls [`Self::build`]
/// with the configured toggles instead; this exists only to satisfy the
/// trait.
#[instrument(skip(_s))]
async fn init(_s: &str) -> Result<Self, ProviderError> {
Self::build(ProviderToggles::all()).await
}
fn settings(&self) -> String {
String::new()
@ -358,7 +410,10 @@ impl ProviderClient for ProviderOrchestrator {
/// Routes to the provider that owns the path.
fn is_track_path(&self, path: &str) -> bool {
if path == "/tidal" || path.starts_with("/tidal/") {
return self.tidal_client.is_track_path(path);
return self
.tidal_client
.as_ref()
.is_some_and(|tidal| tidal.is_track_path(path));
}
if fs_owns(path) {
return self
@ -388,7 +443,7 @@ impl ProviderClient for ProviderOrchestrator {
#[instrument(skip(self))]
async fn get_urls_for_track(&self, track_path: &str) -> Result<Vec<String>, ProviderError> {
if track_path.starts_with("/tidal/") {
return self.tidal_client.get_urls_for_track(track_path).await;
return self.tidal_provider()?.get_urls_for_track(track_path).await;
}
if fs_owns(track_path) {
return self.fs_provider()?.get_urls_for_track(track_path).await;
@ -418,7 +473,10 @@ impl ProviderClient for ProviderOrchestrator {
#[instrument(skip(self))]
async fn get_metadata_for_track(&self, track_path: &str) -> Result<Track, ProviderError> {
if track_path.starts_with("/tidal/") {
return self.tidal_client.get_metadata_for_track(track_path).await;
return self
.tidal_provider()?
.get_metadata_for_track(track_path)
.await;
}
if fs_owns(track_path) {
return self.fs_provider()?.get_metadata_for_track(track_path).await;
@ -447,9 +505,11 @@ impl ProviderClient for ProviderOrchestrator {
fn get_lib_root(&self) -> LibraryNode {
let mut root_node = LibraryNode::new();
if self.tidal_client.is_some() {
let child =
LibraryNodeChild::new(tidaldy::PROVIDER_ROOT.to_owned(), "tidal".to_owned(), false);
root_node.children.push(child);
}
if self.fs_client.is_some() {
let child =
LibraryNodeChild::new(fsdy::PROVIDER_ROOT.to_owned(), "fs".to_owned(), false);
@ -486,7 +546,7 @@ impl ProviderClient for ProviderOrchestrator {
return Ok(self.get_lib_root());
}
let mut node = if path == tidaldy::PROVIDER_ROOT || path.starts_with("/tidal/") {
self.tidal_client.get_lib_node(path).await?
self.tidal_provider()?.get_lib_node(path).await?
} else if fs_owns(path) {
self.fs_provider()?.get_lib_node(path).await?
} else if crabidy_owns(path) {
@ -516,7 +576,10 @@ impl ProviderClient for ProviderOrchestrator {
title: &str,
) -> Result<LibraryNode, ProviderError> {
if parent_path == tidaldy::PROVIDER_ROOT || parent_path.starts_with("/tidal/") {
return self.tidal_client.create_lib_node(parent_path, title).await;
return self
.tidal_provider()?
.create_lib_node(parent_path, title)
.await;
}
if fs_owns(parent_path) {
return self
@ -555,7 +618,10 @@ impl ProviderClient for ProviderOrchestrator {
new_title: &str,
) -> Result<LibraryNode, ProviderError> {
if path == tidaldy::PROVIDER_ROOT || path.starts_with("/tidal/") {
return self.tidal_client.rename_lib_node(path, new_title).await;
return self
.tidal_provider()?
.rename_lib_node(path, new_title)
.await;
}
if fs_owns(path) {
return self.fs_provider()?.rename_lib_node(path, new_title).await;
@ -591,7 +657,10 @@ impl ProviderClient for ProviderOrchestrator {
chunk_tx: flume::Sender<Vec<Track>>,
) -> Result<(), ProviderError> {
if path == tidaldy::PROVIDER_ROOT || path.starts_with("/tidal/") {
return self.tidal_client.resolve_tracks_into(path, chunk_tx).await;
return self
.tidal_provider()?
.resolve_tracks_into(path, chunk_tx)
.await;
}
if fs_owns(path) {
return self
@ -626,7 +695,7 @@ impl ProviderClient for ProviderOrchestrator {
#[instrument(skip(self))]
async fn delete_lib_node(&self, path: &str) -> Result<LibraryNode, ProviderError> {
if path == tidaldy::PROVIDER_ROOT || path.starts_with("/tidal/") {
return self.tidal_client.delete_lib_node(path).await;
return self.tidal_provider()?.delete_lib_node(path).await;
}
if fs_owns(path) {
return self.fs_provider()?.delete_lib_node(path).await;

View File

@ -1,11 +1,12 @@
//! Server-level configuration: `~/.config/crabidy/crabidy-server.toml`.
//!
//! Today this only carries the `[auth]` role hashes
//! (architecture/roles-auth.md). The file is optional — a missing file
//! runs the server open, exactly as before the feature — but a file
//! that exists and does not parse aborts startup: silently ignoring a
//! broken auth config would run an intended-to-be-locked server open
//! (fail-closed, quality/roles-auth.md).
//! Carries the enabled-providers list ([`ServerSettings::providers`]) and the
//! `[auth]` role hashes (architecture/roles-auth.md). The server writes a
//! default (all providers, no auth) on first start via
//! [`ServerSettings::ensure_default`]. The file is optional — a missing file
//! runs the server open with every provider — but a file that exists and does
//! not parse aborts startup: silently ignoring a broken auth config would run
//! an intended-to-be-locked server open (fail-closed, quality/roles-auth.md).
use std::path::Path;
@ -14,15 +15,51 @@ use serde::{Deserialize, Serialize};
/// The server config file name inside the crabidy config directory.
pub const SETTINGS_FILE: &str = "crabidy-server.toml";
/// Every built-in provider, in the order the default config lists them. Each
/// name is a library root (`/tidal`, `/youtube`, `/fs`, `/crabidy`,
/// `/orphans`). `orphans` is a view over the store, so it needs `crabidy`.
pub const ALL_PROVIDERS: [&str; 5] = ["tidal", "youtube", "fs", "crabidy", "orphans"];
/// Contents of `crabidy-server.toml`.
#[derive(Debug, Default, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ServerSettings {
/// Enabled providers. The default config we write lists all of them
/// ([`ALL_PROVIDERS`]); remove a name to disable that provider. Absent —
/// the key deleted, or a fresh install with no file — enables all of them,
/// so a server never silently loses every provider (fail-open for the
/// library, unlike auth).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub providers: Option<Vec<String>>,
/// Role credentials; absent (or empty) means the server runs open.
#[serde(default)]
#[serde(default, skip_serializing_if = "AuthSettings::is_default")]
pub auth: AuthSettings,
}
/// Which providers to mount, resolved from [`ServerSettings::providers`].
#[derive(Debug, Clone, Copy)]
pub struct ProviderToggles {
pub tidal: bool,
pub youtube: bool,
pub fs: bool,
pub crabidy: bool,
pub orphans: bool,
}
impl ProviderToggles {
/// Every provider on — the default, and what a missing/keyless config
/// yields.
pub fn all() -> Self {
Self {
tidal: true,
youtube: true,
fs: true,
crabidy: true,
orphans: true,
}
}
}
/// One PHC password hash per role; a role without a hash cannot
/// authenticate. Generate hashes with `crabidy-server guard <role>`.
/// Hashes are not passwords, but the file should stay private anyway.
@ -43,6 +80,12 @@ impl AuthSettings {
pub fn enabled(&self) -> bool {
self.owner.is_some() || self.queue_owner.is_some() || self.queue_appender.is_some()
}
/// Whether this is the empty default (no roles) — so the auto-written
/// default config omits the `[auth]` table entirely.
fn is_default(&self) -> bool {
self.owner.is_none() && self.queue_owner.is_none() && self.queue_appender.is_none()
}
}
impl ServerSettings {
@ -63,7 +106,46 @@ impl ServerSettings {
toml::from_str(&raw).map_err(|err| format!("invalid {}: {err}", file.display()))
}
/// Serializes the current `[auth]` back to `crabidy-server.toml` in
/// Whether the named provider is enabled: every provider when the
/// `providers` key is absent, otherwise only the names the list holds.
pub fn provider_enabled(&self, name: &str) -> bool {
self.providers
.as_ref()
.is_none_or(|list| list.iter().any(|p| p == name))
}
/// The per-provider mount decisions for the orchestrator. `orphans` also
/// requires `crabidy` (it is a view over the store); that dependency is
/// enforced where the store is built, so it is not folded in here.
pub fn provider_toggles(&self) -> ProviderToggles {
ProviderToggles {
tidal: self.provider_enabled("tidal"),
youtube: self.provider_enabled("youtube"),
fs: self.provider_enabled("fs"),
crabidy: self.provider_enabled("crabidy"),
orphans: self.provider_enabled("orphans"),
}
}
/// Writes a default `crabidy-server.toml` — all providers enabled, no auth
/// — when none exists yet, so users have a full list to prune. A file that
/// already exists (even a pruned one) is left untouched. Best-effort: the
/// caller treats a write failure as a warning, not a startup error.
pub fn ensure_default(config_dir: &Path) -> Result<(), String> {
let file = config_dir.join(SETTINGS_FILE);
match std::fs::metadata(&file) {
Ok(_) => return Ok(()),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(err) => return Err(format!("cannot check {}: {err}", file.display())),
}
let settings = ServerSettings {
providers: Some(ALL_PROVIDERS.iter().map(|s| s.to_string()).collect()),
auth: AuthSettings::default(),
};
settings.store(config_dir)
}
/// Serializes the current settings back to `crabidy-server.toml` in
/// `config_dir`, creating the directory if missing.
///
/// The flat `[auth]` shape is preserved (each role a top-level key), so a
@ -143,6 +225,79 @@ mod tests {
assert!(nested.join(SETTINGS_FILE).is_file());
}
#[test]
fn an_absent_providers_key_enables_everything() {
let settings = ServerSettings::default();
for provider in ALL_PROVIDERS {
assert!(settings.provider_enabled(provider), "{provider}");
}
let toggles = settings.provider_toggles();
assert!(
toggles.tidal && toggles.youtube && toggles.fs && toggles.crabidy && toggles.orphans
);
}
#[test]
fn an_explicit_list_enables_only_its_names() {
let dir = TempDir::new().expect("tempdir");
std::fs::write(
dir.path().join(SETTINGS_FILE),
"providers = [\"fs\", \"crabidy\"]\n",
)
.expect("write");
let settings = ServerSettings::load(dir.path()).expect("load");
assert!(settings.provider_enabled("fs") && settings.provider_enabled("crabidy"));
assert!(!settings.provider_enabled("tidal"));
assert!(!settings.provider_enabled("youtube"));
assert!(!settings.provider_enabled("orphans"));
let toggles = settings.provider_toggles();
assert!(toggles.fs && toggles.crabidy);
assert!(!toggles.tidal && !toggles.youtube && !toggles.orphans);
}
#[test]
fn ensure_default_writes_all_providers_and_never_clobbers() {
let dir = TempDir::new().expect("tempdir");
ServerSettings::ensure_default(dir.path()).expect("write default");
let text = std::fs::read_to_string(dir.path().join(SETTINGS_FILE)).expect("read");
for provider in ALL_PROVIDERS {
assert!(text.contains(provider), "default lists {provider}");
}
assert!(
!text.contains("[auth]"),
"no empty auth table in the default"
);
let reloaded = ServerSettings::load(dir.path()).expect("reload");
assert_eq!(
reloaded.providers.as_ref().map(Vec::len),
Some(ALL_PROVIDERS.len())
);
// A second call must not overwrite a file the user has since pruned.
std::fs::write(dir.path().join(SETTINGS_FILE), "providers = [\"fs\"]\n").expect("prune");
ServerSettings::ensure_default(dir.path()).expect("no-op");
let after = std::fs::read_to_string(dir.path().join(SETTINGS_FILE)).expect("read");
assert_eq!(after.trim(), "providers = [\"fs\"]");
}
#[test]
fn providers_and_auth_round_trip() {
let dir = TempDir::new().expect("tempdir");
std::fs::write(
dir.path().join(SETTINGS_FILE),
"providers = [\"tidal\", \"fs\"]\n[auth]\nowner = \"$argon2id$x\"\n",
)
.expect("seed");
let settings = ServerSettings::load(dir.path()).expect("load");
assert!(settings.auth.enabled());
settings.store(dir.path()).expect("store");
let reloaded = ServerSettings::load(dir.path()).expect("reload");
assert_eq!(
reloaded.providers,
Some(vec!["tidal".to_string(), "fs".to_string()])
);
assert_eq!(reloaded.auth.owner.as_deref(), Some("$argon2id$x"));
}
#[test]
fn a_broken_file_is_a_startup_error_not_an_open_server() {
let dir = TempDir::new().expect("tempdir");

View File

@ -6,11 +6,12 @@ Crabidy reads its configuration from TOML files in `~/.config/crabidy/`,
the platform config directory. Every file is optional. On first start
each component writes its own file filled in with defaults, then reads
it back — so a fresh install runs with sensible values and leaves you an
editable file for each piece. A provider whose file is missing simply
does not mount; it does not stop the server.
editable file for each piece. A provider whose config fails to load
simply does not mount; it does not stop the server.
The one exception is the server auth file, `crabidy-server.toml`: it is
never auto-created (see [Roles and authorization](./auth.md)).
Which providers mount at all is controlled by `crabidy-server.toml` (see
[Enabling and disabling providers](#enabling-and-disabling-providers)),
which the server now also writes on first start.
## The config files
@ -21,14 +22,29 @@ never auto-created (see [Roles and authorization](./auth.md)).
| `fsdy.toml` | local fs | yes |
| `cbd-tui.toml` | `cbd-tui` | yes |
| `cbd.toml` | `cbd` | yes |
| `crabidy-server.toml` | server | no |
| `crabidy-server.toml` | server | yes |
- `tidaly.toml`, `ytdy.toml`, and `fsdy.toml` configure the three media
providers — Tidal, YouTube, and a local music folder (its filesystem
root). See [Providers](./providers.md).
- `cbd-tui.toml` and `cbd.toml` are client configs (below).
- `crabidy-server.toml` holds server auth (see
[Roles and authorization](./auth.md)).
- `crabidy-server.toml` holds the enabled-providers list (below) and server
auth (see [Roles and authorization](./auth.md)).
## Enabling and disabling providers
On first start the server writes `crabidy-server.toml` with every provider
enabled:
```toml
providers = ["tidal", "youtube", "fs", "crabidy", "orphans"]
```
**Remove a name to disable that provider** — it no longer mounts and drops
out of the library tree; its own config file (e.g. `tidaly.toml`) is then
left unread. Deleting the whole `providers` line re-enables everything (the
same as a fresh install with no file). Because `orphans` is a view over the
store, disabling `crabidy` disables `orphans` too.
## Client config: `cbd-tui.toml` and `cbd.toml`