qbt-clean 0.124.0

Automated rules-based cleaning of qBittorrent torrents.
#[derive(Debug)]
pub struct Group {
	pub pinned: Option<crate::PinReason>,
	size: u64, // The logical group size.
	pub torrents: Vec<crate::GroupTorrent>,
}

impl Group {
	pub fn new(size: u64) -> Self {
		Group {
			pinned: None,
			size,
			torrents: Vec::new(),
		}
	}
}

impl Group {
	fn counted_copies(&self) -> usize {
		self.scoring_torrents().count()
	}

	fn last_activity(&self, now: std::time::SystemTime) -> std::time::Duration {
		self.scoring_torrents()
			.map(|t| t.torrent.last_activity.duration_since(now).unwrap_or_default())
			.min()
			.unwrap_or(std::time::Duration::ZERO)
	}

	fn max_age(&self, now: std::time::SystemTime) -> std::time::Duration {
		self.scoring_torrents()
			.map(|t| t.torrent.added_on.duration_since(now).unwrap_or_default())
			.max()
			.unwrap_or(std::time::Duration::ZERO)
	}

	fn min_age(&self, now: std::time::SystemTime) -> std::time::Duration {
		self.scoring_torrents()
			.map(|t| t.torrent.added_on.duration_since(now).unwrap_or_default())
			.min()
			.unwrap_or(std::time::Duration::ZERO)
	}

	fn no_counted_torrents_score(&self, global: &crate::Global<impl crate::Qbt>) -> f64 {
		if self.scoring_torrents().next().is_none() {
			global.config.no_scored_torrents_weight
		} else {
			0.0
		}
	}

	pub fn rank(&self, global: &crate::Global<impl crate::Qbt>) -> f64 {
		self.content_size() as f64 * global.config.size_weight
		+ self.counted_copies() as f64 * global.config.copies_weight
		+ self.last_activity(global.now).as_secs_f64() / 3600.0 / 24.0 * global.config.last_activity_d_weight
		+ self.max_age(global.now).as_secs_f64() / 3600.0 / 24.0 * global.config.age_d_max_weight
		+ self.min_age(global.now).as_secs_f64() / 3600.0 / 24.0 * global.config.age_d_min_weight
		+ self.no_counted_torrents_score(global)
		+ self.seeders() as f64 * global.config.seeder_count_weight
	}

	fn scoring_torrents(&self) -> impl Iterator<Item=&crate::GroupTorrent> {
		self.torrents.iter()
			.filter(|t| t.include_in_score)
	}

	fn seeders(&self) -> u64 {
		self.scoring_torrents()
			.map(|t| t.torrent.seeders)
			.sum()
	}

	pub fn group_id(&self) -> u64 {
		self.size
	}

	/// Best guess of the amount of data that will be recovered by deleting this torrent.
	pub fn content_size(&self) -> u64 {
		// In theory we should try to identify distinct different files and count them. But in practice this will be close enough.
		self.torrents.iter()
			.map(|t| t.torrent.selected_size)
			.max()
			.unwrap()
	}
}