use async_trait::async_trait;
use log::debug;
use reqwest::{Client, Request};
use crate::{errors::ClientError, SearchRequest, Torrent};
pub mod knaben;
pub mod piratebay;
pub mod yts;
pub use knaben::Knaben;
pub use piratebay::PirateBay;
pub use yts::Yts;
#[async_trait]
pub trait SearchProvider: Send + Sync {
async fn send_request(
&self,
client: &Client,
request: SearchRequest<'_>,
) -> Result<Vec<Torrent>, ClientError> {
let request = self.build_request(client, request)?;
debug!(
"client sending {} request to {} with {} bytes of data",
request.method(),
request.url(),
request.body().as_slice().len()
);
let response = client
.execute(request)
.await
.map_err(|e| ClientError::ResponseError(e.into()))?;
let response_status = response.status();
let response_content = response
.text()
.await
.map_err(|e| ClientError::ResponseError(e.into()))?;
debug!(
"client received {} response with {} bytes of body data",
response_status,
response_content.len()
);
if !response_status.is_success() {
return Err(ClientError::ServerResponseError {
code: response_status,
content: response_content.clone(),
});
}
self.parse_response(&response_content)
}
fn parse_response(&self, response: &str) -> Result<Vec<Torrent>, ClientError>;
fn build_request(
&self,
client: &Client,
request: SearchRequest<'_>,
) -> Result<Request, ClientError>;
fn id(&self) -> String;
}
#[cfg(test)]
mod tests {
use core::panic;
use super::*;
use async_trait::async_trait;
use mockito::Server;
use reqwest::Client;
use serde_json::json;
struct MockProvider {
url: String,
}
impl MockProvider {
fn new(url: &str) -> Self {
Self {
url: url.to_string(),
}
}
}
#[async_trait]
impl SearchProvider for MockProvider {
fn parse_response(&self, response: &str) -> Result<Vec<Torrent>, ClientError> {
let parsed: Vec<Torrent> = serde_json::from_str(response)
.map_err(|e| ClientError::DataParseError(e.into()))?;
Ok(parsed)
}
fn build_request(
&self,
client: &Client,
request: SearchRequest<'_>,
) -> Result<Request, ClientError> {
let request = client
.get(format!("{}/search", self.url.clone()))
.query(&[("q", request.query)])
.build()
.unwrap();
Ok(request)
}
fn id(&self) -> String {
self.url.clone()
}
}
#[tokio::test]
async fn test_send_request_success() {
let mut server = Server::new_async().await;
let provider = MockProvider::new(&server.url());
let client = Client::new();
let _mock = server
.mock("GET", "/search?q=ubuntu")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
json!([{
"name": "Ubuntu ISO",
"magnet_link": "magnet:?xt=urn:btih:abc123",
"seeders": 20,
"peers": 10,
"size_bytes": 2048,
"provider": server.url()
}])
.to_string(),
)
.create();
let search_request = SearchRequest::new("ubuntu");
let result = provider.send_request(&client, search_request).await;
assert!(result.is_ok());
let torrents = result.unwrap();
assert_eq!(torrents.len(), 1);
let torrent = &torrents[0];
assert_eq!(torrent.name, "Ubuntu ISO");
assert_eq!(torrent.magnet_link, "magnet:?xt=urn:btih:abc123");
assert_eq!(torrent.seeders, 20);
assert_eq!(torrent.peers, 10);
assert_eq!(torrent.size_bytes, 2048);
assert_eq!(torrent.provider, server.url());
}
#[tokio::test]
async fn test_send_request_error_response() {
let mut server = Server::new_async().await;
let provider = MockProvider::new(&server.url());
let client = Client::new();
let _mock = server
.mock("GET", "/search?q=ubuntu")
.with_status(500)
.with_body("Internal Server Error")
.create();
let search_request = SearchRequest::new("ubuntu");
let result = provider.send_request(&client, search_request).await;
assert!(result.is_err());
if let ClientError::ServerResponseError { code, content } = result.unwrap_err() {
assert_eq!(code.as_u16(), 500);
assert_eq!(content, "Internal Server Error");
} else {
panic!("Expected ServerResponseError");
}
}
}