use std::fmt;
use std::str::FromStr;
use std::sync::Arc;
use axum::extract::{Path, State};
use axum::response::{IntoResponse, Response};
use axum_extra::extract::Query;
use serde::{de, Deserialize, Deserializer};
use thiserror::Error;
use torrust_tracker_primitives::info_hash::InfoHash;
use torrust_tracker_primitives::pagination::Pagination;
use super::responses::{torrent_info_response, torrent_list_response, torrent_not_known_response};
use crate::core::services::torrent::{get_torrent_info, get_torrents, get_torrents_page};
use crate::core::Tracker;
use crate::servers::apis::v1::responses::invalid_info_hash_param_response;
use crate::servers::apis::InfoHashParam;
pub async fn get_torrent_handler(State(tracker): State<Arc<Tracker>>, Path(info_hash): Path<InfoHashParam>) -> Response {
match InfoHash::from_str(&info_hash.0) {
Err(_) => invalid_info_hash_param_response(&info_hash.0),
Ok(info_hash) => match get_torrent_info(tracker.clone(), &info_hash).await {
Some(info) => torrent_info_response(info).into_response(),
None => torrent_not_known_response(),
},
}
}
#[derive(Deserialize, Debug)]
pub struct QueryParams {
#[serde(default, deserialize_with = "empty_string_as_none")]
pub offset: Option<u32>,
#[serde(default, deserialize_with = "empty_string_as_none")]
pub limit: Option<u32>,
#[serde(default, rename = "info_hash")]
pub info_hashes: Vec<String>,
}
pub async fn get_torrents_handler(State(tracker): State<Arc<Tracker>>, pagination: Query<QueryParams>) -> Response {
tracing::debug!("pagination: {:?}", pagination);
if pagination.0.info_hashes.is_empty() {
torrent_list_response(
&get_torrents_page(
tracker.clone(),
Some(&Pagination::new_with_options(pagination.0.offset, pagination.0.limit)),
)
.await,
)
.into_response()
} else {
match parse_info_hashes(pagination.0.info_hashes) {
Ok(info_hashes) => torrent_list_response(&get_torrents(tracker.clone(), &info_hashes).await).into_response(),
Err(err) => match err {
QueryParamError::InvalidInfoHash { info_hash } => invalid_info_hash_param_response(&info_hash),
},
}
}
}
#[derive(Error, Debug)]
pub enum QueryParamError {
#[error("invalid infohash {info_hash}")]
InvalidInfoHash { info_hash: String },
}
fn parse_info_hashes(info_hashes_str: Vec<String>) -> Result<Vec<InfoHash>, QueryParamError> {
let mut info_hashes: Vec<InfoHash> = Vec::new();
for info_hash_str in info_hashes_str {
match InfoHash::from_str(&info_hash_str) {
Ok(info_hash) => info_hashes.push(info_hash),
Err(_err) => {
return Err(QueryParamError::InvalidInfoHash {
info_hash: info_hash_str,
})
}
}
}
Ok(info_hashes)
}
fn empty_string_as_none<'de, D, T>(de: D) -> Result<Option<T>, D::Error>
where
D: Deserializer<'de>,
T: FromStr,
T::Err: fmt::Display,
{
let opt = Option::<String>::deserialize(de)?;
match opt.as_deref() {
None | Some("") => Ok(None),
Some(s) => FromStr::from_str(s).map_err(de::Error::custom).map(Some),
}
}