sauce_api/source/
saucenao.rs

1use async_trait::async_trait;
2use reqwest::{header, Client};
3use serde::{Deserialize, Serialize};
4
5use crate::error::Error;
6
7use super::{Item, Output, Source};
8
9/// The [`SauceNao`] source.
10/// Requires an API key to function.
11///
12/// Works with `saucenao.com`
13#[derive(Debug)]
14pub struct SauceNao {
15    /// The API key to use.
16    api_key: String,
17}
18
19#[async_trait]
20impl Source for SauceNao {
21    type Argument = String;
22
23    async fn check(&self, url: &str) -> Result<Output, Error> {
24        let client = Client::new();
25
26        // Check whether we're dealing with an image
27        let head = client.head(url).send().await?;
28
29        let content_type = head.headers().get(header::CONTENT_TYPE);
30
31        if let Some(content_type) = content_type {
32            let content_type = content_type.to_str()?;
33
34            if !content_type.contains("image") {
35                return Err(Error::LinkIsNotImage);
36            }
37        } else {
38            return Err(Error::LinkIsNotImage);
39        }
40
41        // Build the request
42
43        let req = {
44            client
45                .get("https://saucenao.com/search.php")
46                .query(&Query::default().url(url).api_key(&self.api_key))
47                .header(header::ACCEPT_ENCODING, "utf-8")
48        };
49
50        // Send the request
51
52        let resp = req.send().await?;
53
54        // Parse the response
55
56        let text = resp.text().await?;
57        let json: ApiResponse = serde_json::from_str(&text)?;
58
59        let mut result = Output {
60            original_url: url.to_string(),
61            items: Vec::new(),
62        };
63
64        for item in json.results {
65            if let Some(links) = item.data.ext_urls {
66                let item = Item {
67                    similarity: item.header.similarity.parse::<f32>()?,
68                    link: links[0].clone(),
69                };
70
71                result.items.push(item);
72            }
73        }
74
75        Ok(result)
76    }
77
78    async fn create(arg: Self::Argument) -> Result<Self, Error> {
79        Ok(Self { api_key: arg })
80    }
81}
82
83#[derive(Debug, Serialize)]
84struct Query {
85    url: String,
86    api_key: String,
87    db: u16,
88    output_type: u8,
89    #[serde(rename = "testmode")]
90    test_mode: u8,
91    #[serde(rename = "numres")]
92    num_res: u8,
93}
94
95impl Query {
96    pub fn url(mut self, url: &str) -> Self {
97        self.url = url.to_string();
98        self
99    }
100
101    pub fn api_key(mut self, api_key: &str) -> Self {
102        self.api_key = api_key.to_string();
103        self
104    }
105}
106
107impl Default for Query {
108    fn default() -> Self {
109        Self {
110            url: String::new(),
111            api_key: String::new(),
112            db: 999,
113            output_type: 2,
114            test_mode: 1,
115            num_res: 16,
116        }
117    }
118}
119
120#[derive(Debug, Deserialize)]
121struct ApiResponse {
122    results: Vec<ApiItem>,
123}
124
125#[derive(Debug, Deserialize)]
126struct ApiItem {
127    header: ApiItemHeader,
128    data: ApiItemData,
129}
130
131#[derive(Debug, Deserialize)]
132struct ApiItemHeader {
133    similarity: String,
134}
135
136#[derive(Debug, Deserialize)]
137struct ApiItemData {
138    ext_urls: Option<Vec<String>>,
139}