qbt-clean 0.122.0

Automated rules-based cleaning of qBittorrent torrents.
pub async fn cmd_clean<Q: crate::Qbt>(
	global: &crate::Global<Q>,
	dry_run: bool,
) -> anyhow::Result<()> {
	let server_state = global.qbt.server_state().await?;

	let Some(to_free) = global.config.target_free.checked_sub(server_state.free_space_on_disk)
		else {
			eprintln!(
				"Have {} free which satisfies target_free of {}, nothing to do.",
				crate::FmtBytes(server_state.free_space_on_disk),
				crate::FmtBytes(global.config.target_free));
			return Ok(())
		};

	eprintln!(
		"Going to free {} to hit target of {} free space.",
		crate::FmtBytes(to_free),
		crate::FmtBytes(global.config.target_free));

	let now = std::time::SystemTime::now();

	let mut groups = global.group(None).await?
		.filter(|g| g.pinned.is_none())
		.collect::<Vec<_>>();
	groups.sort_unstable_by_key(|g| {
		std::cmp::Reverse(crate::F64Ord(g.rank(&global)))
	});
	let mut groups = groups.into_iter();

	let mut removed_bytes = 0u64;
	let mut removed_groups = 0u64;
	let mut removed_torrents = 0u64;

	let mut hashes = Vec::new();
	for g in &mut groups {
		removed_bytes += g.content_size();
		removed_groups += 1;

		eprintln!(
			"--- group {}, torrents: {} ---",
			crate::FmtBytes(g.group_id()),
			g.torrents.len());

		hashes.clear();
		for t in g.torrents {
			let last_active = now.duration_since(t.torrent.last_activity)
				.unwrap_or(std::time::Duration::ZERO);

			eprintln!(
				"DELETE {} {:?} category: {}, tracker: {:?}, seeders: {}, active: {} ratio: {:.02}",
				t.torrent.hash,
				t.torrent.name,
				t.torrent.category,
				t.torrent.tracker_host().as_deref().unwrap_or(&t.torrent.tracker),
				t.torrent.seeders,
				crate::FmtDuration(last_active),
				t.torrent.ratio);

			hashes.push(t.torrent.hash);
			removed_torrents += 1;
		}

		if !dry_run {
			global.qbt.delete_torrents(&hashes).await?;
		}

		if removed_bytes >= to_free { break }
	}

	eprintln!(
		"Deleted {} torrents in {} groups. Aprox {} freed.",
		removed_torrents,
		removed_groups,
		crate::FmtBytes(removed_bytes));

	if to_free > removed_bytes {
		eprintln!("No more eligible torrents to reach free space target.");
	} else {
		let remaining = groups.map(|g| g.content_size()).sum();
		eprintln!("Free space target met. {} still available to clean.", crate::FmtBytes(remaining));
	}

	Ok(())
}