qbt-clean 0.149.0

Automated rules-based cleaning of qBittorrent torrents.
#[serde_with::serde_as]
#[derive(Clone,Debug,Default,serde::Deserialize)]
#[serde(deny_unknown_fields)]
#[serde(remote="Self")]
#[serde(rename_all="kebab-case")]
pub struct Rule {
	#[serde(rename="match")] pub match_: crate::Match,

	#[serde_as(as="Option<serde_with::FromInto<crate::SerdeDuration>>")] pub last_activity_min: Option<std::time::Duration>,
	#[serde_as(as="Option<serde_with::FromInto<crate::SerdeDuration>>")] pub seed_time_min: Option<std::time::Duration>,
	pub categories_allowed: Option<std::collections::HashSet<String>>,
	pub include_in_score: Option<bool>,
	pub pin: Option<bool>,
	pub pin_reason: Option<String>,
	pub seeder_count_min: Option<u64>,
	pub state_allowed: Option<std::collections::HashSet<crate::TorrentState>>,
}

impl<'de> serde::Deserialize<'de> for Rule {
	fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
		let rule = Rule::deserialize(deserializer)?;
		rule.validate().map_err(serde::de::Error::custom)?;
		Ok(rule)
	}
}

impl Rule {
	fn validate(&self) -> anyhow::Result<()> {
		if let Some(reason) = &self.pin_reason {
			anyhow::ensure!(
				self.pin.is_some(),
				"pin-reason {reason:?} is set but pin is not. A pin-reason is only meaningful on a rule that sets pin.");
		}

		Ok(())
	}
}