qbt-clean 0.153.0

Automated rules-based cleaning of qBittorrent torrents.
#[serde_with::serde_as]
#[derive(Clone,Debug,Default,serde::Deserialize)]
#[serde(deny_unknown_fields)]
#[serde(rename_all="kebab-case")]
pub struct Match {
	pub any: Option<Vec<Match>>,
	pub categories: Option<std::collections::HashSet<String>>,
	pub is_private: Option<bool>,
	#[serde_as(as="Option<crate::Cmp<crate::SerdeDuration>>")] pub last_activity: Option<crate::Cmp<std::time::Duration>>,
	pub leech_count: Option<crate::Cmp<u64>>,
	#[serde_as(as="Option<serde_with::FromInto<crate::SerdeRegexSet>>")] pub name: Option<regex::RegexSet>,
	#[serde(default)] pub none: Vec<Match>,
	pub ratio: Option<crate::Cmp<f32>>,
	#[serde_as(as="Option<crate::Cmp<crate::SerdeDuration>>")] pub seed_time: Option<crate::Cmp<std::time::Duration>>,
	pub seed_count: Option<crate::Cmp<u64>>,
	pub state: Option<std::collections::HashSet<crate::TorrentState>>,
	#[serde_as(as="Option<serde_with::FromInto<crate::SerdeRegexSet>>")] pub tracker_msg: Option<regex::RegexSet>,
	pub tracker_status: Option<std::collections::HashSet<crate::TrackerStatus>>,
	#[serde_as(as="Option<serde_with::FromInto<crate::SerdeRegexSet>>")] pub tracker_url: Option<regex::RegexSet>,
}

impl Match {
	pub fn match_fast(
		&self,
		now: std::time::SystemTime,
		t: &crate::Torrent,
	) -> Option<bool> {
		let mut need_slow = false;

		if self.is_private.is_some_and(|p| Some(p) != t.private) {
			return Some(false)
		}

		if let Some(last_activity) = &self.last_activity {
			let since = now.duration_since(t.last_activity)
				.unwrap_or_default();

			if !last_activity.matches(&since) {
				return Some(false)
			}
		}

		if self.ratio.as_ref().is_some_and(|r| !r.matches(&t.ratio)) {
			return Some(false)
		}

		if self.seed_time.as_ref().is_some_and(|s| !s.matches(&t.seeding_time)) {
			return Some(false)
		}

		if self.leech_count.as_ref().is_some_and(|l| !l.matches(&t.leechers)) {
			return Some(false)
		}

		if self.seed_count.as_ref().is_some_and(|s| !s.matches(&t.seeders)) {
			return Some(false)
		}

		if self.state.as_ref().is_some_and(|s| !s.contains(&t.state)) {
			return Some(false)
		}

		if self.categories.as_ref().is_some_and(|c| !c.contains(&t.category)) {
			return Some(false)
		}

		if self.name.as_ref().is_some_and(|n| !n.is_match(&t.name)) {
			return Some(false)
		}

		for pred in &self.none {
			match pred.match_fast(now, t) {
				Some(true) => return Some(false),
				Some(false) => continue,
				None => need_slow = true,
			}
		}

		if let Some(any) = &self.any {
			let mut r = Some(false);
			for a in any {
				match a.match_fast(now, t) {
					Some(true) => {
						r = Some(true);
						break
					},
					Some(false) => continue,
					None => r = None,
				}
			}
			match r {
				Some(true) => {}
				Some(false) => return Some(false),
				None => need_slow = true,
			}
		}

		need_slow |=
			self.tracker_msg.is_some()
			|| self.tracker_status.is_some()
			|| self.tracker_url.is_some();

		if need_slow {
			None
		} else {
			Some(true)
		}
	}

	pub fn match_slow(
		&self,
		trackers: &[crate::Tracker],
	) -> bool {
		for pred in &self.none {
			if pred.match_slow(trackers) {
				return false
			}
		}

		if let Some(any) = &self.any {
			let mut r = false;
			for a in any {
				if a.match_slow(trackers) {
					r = true;
					break
				}
			}
			if !r {
				return false
			}
		}

		if let Some(tracker_msg) = &self.tracker_msg {
			let mut r = false;
			for tr in trackers {
				if tracker_msg.is_match(&tr.msg) {
					r = true;
					break
				}
			}
			if !r {
				return false
			}
		}

		if self.tracker_status.as_ref()
			.is_some_and(|s|
				trackers.iter().all(|tr| !s.contains(&tr.status)))
		{
			return false
		}

		if let Some(tracker_url) = &self.tracker_url {
			let mut r = false;
			for tr in trackers {
				if tracker_url.is_match(&tr.url) {
					r = true;
					break
				}
			}
			if !r {
				return false
			}
		}

		true
	}
}