qbt-clean 0.126.0

Automated rules-based cleaning of qBittorrent torrents.
use anyhow::Context as _;

pub struct QbtHttp {
	http: reqwest::Client,
	url: url::Url,
}

impl QbtHttp {
	pub async fn new(
		mut url: url::Url,
		headers: reqwest::header::HeaderMap,
	) -> anyhow::Result<Self> {
		let username = url.username().to_string();
		let password = url.password().unwrap_or("").to_string();
		url.set_username("")
			.map_err(|()| anyhow::Error::msg("Invalid qbt_url"))?;
		url.set_password(None)
			.map_err(|()| anyhow::Error::msg("Invalid qbt_url"))?;

		let http = reqwest::Client::builder()
			.user_agent("qbt-clean/0")
			.connect_timeout(std::time::Duration::from_secs(60))
			.default_headers(headers)
			.timeout(std::time::Duration::from_secs(600))
			.cookie_store(true)
			.build()?;

		http.post(url.join("/api/v2/auth/login")?)
			.form(&[
				("username", &username),
				("password", &password),
			])
			.send().await.context("Sending login request")?
			.error_for_status().context("Login request status")?;

		Ok(Self { http, url })
	}
}

impl crate::Qbt for QbtHttp {
	async fn logout(self) -> anyhow::Result<()> {
		self.http.post(self.url.join("/api/v2/auth/logout")?)
			.send().await?
			.error_for_status()?;
		Ok(())
	}

	async fn server_state(&self) -> anyhow::Result<crate::ServerState> {
		#[derive(serde::Deserialize)]
		struct Sync {
			server_state: crate::ServerState,
		}
		Ok(self.http.get(self.url.join("/api/v2/sync/maindata")?)
			.send().await?
			.error_for_status()?
			.json::<Sync>().await?
			.server_state)
	}

	async fn torrents_info(&self) -> anyhow::Result<Vec<crate::Torrent>> {
		Ok(self.http.get(self.url.join("/api/v2/torrents/info")?)
			.send().await?
			.error_for_status()?
			.json().await?)
	}

	async fn torrent_files(&self, hash: crate::InfoHash)
		-> anyhow::Result<Vec<crate::TorrentFile>>
	{
		Ok(self.http.get(self.url.join("/api/v2/torrents/files")?)
			.query(&[("hash", hash)])
			.send().await?
			.error_for_status()?
			.json().await?)
	}

	async fn torrent_trackers(&self, hash: crate::InfoHash)
		-> anyhow::Result<Vec<crate::Tracker>>
	{
		Ok(self.http.get(self.url.join("/api/v2/torrents/trackers")?)
			.query(&[("hash", hash)])
			.send().await?
			.error_for_status()?
			.json().await?)
	}

	async fn delete_torrents(&self, hashes: &[crate::InfoHash]) -> anyhow::Result<()> {
		let s = hashes.iter()
			.map(|h| h.to_string())
			.collect::<Vec<_>>()
			.join("|");
		self.http.post(self.url.join("/api/v2/torrents/delete")?)
			.form(&[
				("deleteFiles", "true"),
				("hashes", &s),
			])
			.send().await?
			.error_for_status()?;
		Ok(())
	}
}