use mineral_config_macros::{config_section, source_section};
use std::{
num::{NonZeroU32, NonZeroU64, NonZeroUsize},
path::PathBuf,
};
#[config_section]
#[lua_extra_field(
"curate_playlists?",
"mineral.CuratePlaylistsFn",
"跨源策展:各源函数跑完、按注册序合并后的列表(条目带 source 字段),可全局排序/交错"
)]
pub struct SourcesConfig {
netease: NeteaseSection,
bilibili: BilibiliSection,
mineral: MineralSection,
local: LocalSection,
}
#[config_section]
#[lua_extra_field(
"curate_playlists?",
"mineral.CuratePlaylistsFn",
"该源歌单列表的呈现策展(过滤/改名/重排)"
)]
pub struct MineralSection {
backfill: BackfillSection,
}
#[config_section]
pub struct BackfillSection {
chunk_size: usize,
max_concurrent: usize,
}
#[source_section]
#[lua_extra_field(
"curate_playlists?",
"mineral.CuratePlaylistsFn",
"该源歌单(= 收藏夹)列表的呈现策展(过滤/改名/重排)"
)]
pub struct BilibiliSection {}
#[source_section]
#[lua_extra_field(
"curate_playlists?",
"mineral.CuratePlaylistsFn",
"该源歌单列表的呈现策展(过滤/改名/重排)"
)]
pub struct NeteaseSection {
playlist_fetch: PlaylistFetchSection,
requests: NeteaseRequestsSection,
album_cache: NeteaseAlbumCacheSection,
}
#[config_section]
pub struct NeteaseAlbumCacheSection {
#[lua_type("integer")]
ttl_days: u32,
#[lua_type("integer")]
ttl_jitter_days: u32,
}
#[config_section]
pub struct NeteaseRequestsSection {
#[lua_type("integer")]
album_detail_requests_per_second: NonZeroU32,
#[lua_type("integer[]")]
retry_delays_ms: Vec<NonZeroU64>,
}
#[config_section]
pub struct PlaylistFetchSection {
#[lua_type("integer")]
batch_size: NonZeroUsize,
#[lua_type("integer")]
max_concurrent: NonZeroUsize,
}
fn de_proxy<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_any(ProxyVisitor)
}
struct ProxyVisitor;
impl serde::de::Visitor<'_> for ProxyVisitor {
type Value = Option<String>;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("代理 URL 字符串或 `false`")
}
fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
if value {
Err(E::custom(
"proxy 须为代理 URL 字符串或 `false`(禁用),不接受 `true`",
))
} else {
Ok(None)
}
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(Some(value.to_owned()))
}
}
#[cfg(test)]
mod tests {
use super::NeteaseSection;
#[test]
fn playlist_fetch_rejects_zero_limits() {
for value in [
serde_json::json!({"batch_size": 0, "max_concurrent": 3}),
serde_json::json!({"batch_size": 500, "max_concurrent": 0}),
] {
assert!(serde_json::from_value::<super::PlaylistFetchSection>(value).is_err());
}
}
#[test]
fn proxy_false_is_none() -> color_eyre::Result<()> {
let s: NeteaseSection = serde_json::from_value(serde_json::json!({
"album_cache": {"ttl_days": 30, "ttl_jitter_days": 7},
"playlist_fetch": {"batch_size": 500, "max_concurrent": 3},
"requests": {"album_detail_requests_per_second": 1, "retry_delays_ms": [500, 1000, 1500]},
"timeout_secs": 100_u64, "proxy": false, "max_connections": 0_u64,
}))?;
assert_eq!(*s.proxy(), None);
Ok(())
}
#[test]
fn proxy_string_is_some() -> color_eyre::Result<()> {
let s: NeteaseSection = serde_json::from_value(serde_json::json!({
"album_cache": {"ttl_days": 30, "ttl_jitter_days": 7},
"playlist_fetch": {"batch_size": 500, "max_concurrent": 3},
"requests": {"album_detail_requests_per_second": 1, "retry_delays_ms": [500, 1000, 1500]},
"timeout_secs": 100_u64, "proxy": "socks5://127.0.0.1:1080", "max_connections": 0_u64,
}))?;
assert_eq!(s.proxy().as_deref(), Some("socks5://127.0.0.1:1080"));
Ok(())
}
#[test]
fn proxy_true_errors() {
assert!(
serde_json::from_value::<NeteaseSection>(serde_json::json!({
"album_cache": {"ttl_days": 30, "ttl_jitter_days": 7},
"playlist_fetch": {"batch_size": 500, "max_concurrent": 3},
"requests": {"album_detail_requests_per_second": 1, "retry_delays_ms": [500, 1000, 1500]},
"timeout_secs": 100_u64, "proxy": true, "max_connections": 0_u64,
}))
.is_err(),
"proxy = true 应报错"
);
}
}
#[config_section]
#[lua_extra_field(
"curate_playlists?",
"mineral.CuratePlaylistsFn",
"本地歌单列表的呈现策展"
)]
pub struct LocalSection {
roots: Vec<PathBuf>,
}