qbt-clean 0.122.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>,
	#[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 let Some(is_private) = self.is_private {
			if t.private != Some(is_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 let Some(ratio) = &self.ratio {
			if !ratio.matches(&t.ratio) {
				return Some(false)
			}
		}

		if let Some(seed_time) = &self.seed_time {
			if !seed_time.matches(&t.seeding_time) {
				return Some(false)
			}
		}

		if let Some(leech_count) = &self.leech_count {
			if !leech_count.matches(&t.leechers) {
				return Some(false)
			}
		}

		if let Some(seeder_count) = &self.seed_count {
			if !seeder_count.matches(&t.seeders) {
				return Some(false)
			}
		}

		if let Some(state) = &self.state {
			if !state.contains(&t.state) {
				return Some(false)
			}
		}

		if let Some(categories) = &self.categories {
			if !categories.contains(&t.category) {
				return Some(false)
			}
		}

		if let Some(name) = &self.name {
			if !name.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_url.is_some();

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

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

		if let Some(any) = &self.any {
			let mut r = false;
			for a in any {
				if a.match_slow(t, 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 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
	}
}