dapctl 1.0.0

TUI/CLI sync tool for HiFi Digital Audio Players
Documentation
use serde::{Deserialize, Serialize};

/// Top-level sync profile TOML. See `examples/sync-fiio-m21-flac.toml`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyncProfile {
    pub schema_version: u32,
    pub profile: ProfileHeader,
    #[serde(default)]
    pub filters: Filters,
    #[serde(default)]
    pub transfer: Transfer,
    #[serde(default)]
    pub transcode: Transcode,
    #[serde(default)]
    pub selective: Selective,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProfileHeader {
    pub name: String,
    pub source: String,
    pub destination: String,
    pub dap_profile: String,
    pub mode: Mode,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Mode {
    Additive,
    Mirror,
    Selective,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Filters {
    #[serde(default)]
    pub include_globs: Vec<String>,
    #[serde(default)]
    pub exclude_globs: Vec<String>,

    // ── Tag-based filters (all optional; require lofty to read audio headers) ──
    /// Include only files whose artist tag matches one of these (case-insensitive).
    /// Empty = accept all artists.
    #[serde(default)]
    pub include_artists: Vec<String>,
    /// Exclude files whose artist tag matches any of these (case-insensitive).
    #[serde(default)]
    pub exclude_artists: Vec<String>,
    /// Include only files whose genre tag matches one of these (case-insensitive).
    #[serde(default)]
    pub include_genres: Vec<String>,
    /// Exclude files whose genre tag matches any of these (case-insensitive).
    #[serde(default)]
    pub exclude_genres: Vec<String>,
    /// Skip files with sample rate strictly below this value (Hz).
    #[serde(default)]
    pub min_sample_rate_hz: Option<u32>,
    /// Skip files with sample rate strictly above this value (Hz).
    #[serde(default)]
    pub max_sample_rate_hz: Option<u32>,
    /// Skip files with bit depth strictly below this value.
    #[serde(default)]
    pub min_bit_depth: Option<u8>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Transfer {
    #[serde(default = "default_verify")]
    pub verify: Verify,
    #[serde(default = "default_true")]
    pub dry_run_default: bool,
    #[serde(default = "default_parallelism")]
    pub parallelism: usize,
}

impl Default for Transfer {
    fn default() -> Self {
        Self {
            verify: default_verify(),
            dry_run_default: true,
            parallelism: default_parallelism(),
        }
    }
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Verify {
    None,
    SizeMtime,
    Checksum,
}

/// Selective-mode inclusion list. Modified by the TUI and persisted via toml_edit.
/// Ignored when `mode` is `additive` or `mirror`.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Selective {
    /// Relative directory prefixes to include (album-level paths, e.g.
    /// "Tool/Lateralus"). Empty = sync nothing in selective mode.
    #[serde(default)]
    pub include_paths: Vec<String>,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Transcode {
    /// Must be `true` for rules to take effect.
    #[serde(default)]
    pub enabled: bool,
    /// Ordered list of format-conversion rules applied during sync.
    #[serde(default)]
    pub rules: Vec<TranscodeRule>,
}

/// A single ffmpeg transcode rule.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TranscodeRule {
    /// Source file extension to match (case-insensitive), e.g. `"DSF"`.
    pub from: String,
    /// Destination extension to produce, e.g. `"FLAC"`.
    pub to: String,
    /// Extra ffmpeg arguments inserted between `-i <input>` and `<output>`.
    /// Example: `"-ar 176400 -sample_fmt s32"`. Empty = let ffmpeg decide.
    #[serde(default)]
    pub params: String,
}

fn default_verify() -> Verify {
    Verify::SizeMtime
}
fn default_true() -> bool {
    true
}
fn default_parallelism() -> usize {
    4
}