qbt-clean 0.122.0

Automated rules-based cleaning of qBittorrent torrents.
#[derive(Debug,clap::Parser)]
#[clap(version)]
pub struct ArgsGroup {
	/// Only consider torrents with a name matching the provided regex.
	///
	/// Warning: If this filter matches some but not all of the torrents in a group the overall conclusion (pinned, score, ...) may be different than it would when running without a filter.
	#[clap(long)]
	name: Option<regex::Regex>,

	#[clap(long)]
	pinned: Option<bool>,

	#[clap(long)]
	pin_reason: Vec<crate::PinReasonKind>,

	#[clap(long,default_value_t=SortBy::Rank)]
	sort: SortBy,
}

#[derive(Clone,Debug,clap::ValueEnum)]
enum SortBy {
	/// Sort by the rank. The groups that would get cleaned up first will be at the bottom.
	Rank,

	/// Sort by group size. This is useful for identifying split groups.
	Size,
}

impl std::fmt::Display for SortBy {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		f.write_str(match self {
			SortBy::Rank => "rank",
			SortBy::Size => "rank",
		})
	}
}

pub async fn cmd_group<Q: crate::Qbt>(
	mut args: ArgsGroup,
	global: &crate::Global<Q>,
) -> anyhow::Result<()> {
	let now = std::time::SystemTime::now();

	if !args.pin_reason.is_empty() {
		anyhow::ensure!(
			args.pinned != Some(false),
			"--pinned=false and --pin-reason don't make sense together as nothign will match.");
		args.pinned = Some(true);
	}

	let mut groups = global.group(
		args.name.as_ref()).await?
		.filter(|g| {
			if let Some(p) = args.pinned {
				if g.pinned.is_some() != p {
					return false
				}

				if !args.pin_reason.is_empty() && !args.pin_reason.contains(&g.pinned.as_ref().unwrap().kind()) {
					return false
				}
			}

			true
		})
		.collect::<Vec<_>>();

	match args.sort {
		SortBy::Rank => {
			groups.sort_unstable_by_key(|g| (
				std::cmp::Reverse(g.pinned.is_some()),
				crate::F64Ord(g.rank(&global))));
		}
		SortBy::Size => {
			groups.sort_unstable_by_key(|g| g.content_size());
		}
	}

	eprintln!("Created {} groups.", groups.len());

	for mut g in groups {
		eprintln!(
			"GROUP {}, torrents: {}, pinned: {}, score: {:.3}",
			crate::FmtBytes(g.group_id()),
			g.torrents.len(),
			crate::FmtEither(g.pinned.as_ref().ok_or(false)),
			g.rank(&global));

		g.torrents.sort_unstable_by(|a, b|
			a.torrent.cmp_natural_key().cmp(&b.torrent.cmp_natural_key()));

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

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

	Ok(())
}