shindan_maker/
client.rs

1use crate::domain::ShindanDomain;
2use crate::internal;
3use anyhow::{Context, Result};
4use reqwest::{Client, header};
5use scraper::Html;
6use std::time::Duration;
7
8#[cfg(feature = "segments")]
9use crate::models::Segments;
10
11/// A client for interacting with ShindanMaker.
12#[derive(Clone, Debug)]
13pub struct ShindanClient {
14    client: Client,
15    domain: ShindanDomain,
16}
17
18impl ShindanClient {
19    /// Create a new ShindanMaker client.
20    pub fn new(domain: ShindanDomain) -> Result<Self> {
21        const TIMEOUT_SECS: u64 = 30;
22        const USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
23
24        Ok(Self {
25            domain,
26            client: Client::builder()
27                .user_agent(USER_AGENT)
28                .use_rustls_tls()
29                .timeout(Duration::from_secs(TIMEOUT_SECS))
30                .cookie_store(true)
31                .build()?,
32        })
33    }
34
35    /// Fetches and extracts title from a shindan page.
36    pub async fn get_title(&self, id: &str) -> Result<String> {
37        let document = self.fetch_document(id).await?;
38        internal::extract_title(&document)
39    }
40
41    /// Fetches and extracts description from a shindan page.
42    pub async fn get_description(&self, id: &str) -> Result<String> {
43        let document = self.fetch_document(id).await?;
44        internal::extract_description(&document)
45    }
46
47    /// Fetches and extracts both title and description from a shindan page.
48    pub async fn get_title_with_description(&self, id: &str) -> Result<(String, String)> {
49        let document = self.fetch_document(id).await?;
50        Ok((
51            internal::extract_title(&document)?,
52            internal::extract_description(&document)?,
53        ))
54    }
55
56    #[cfg(feature = "segments")]
57    /// Get the segments of a shindan.
58    pub async fn get_segments(&self, id: &str, name: &str) -> Result<Segments> {
59        let (_, response_text) = self.submit_shindan(id, name, false).await?;
60        internal::parse_segments(&response_text)
61    }
62
63    #[cfg(feature = "segments")]
64    /// Get the segments of a shindan and the title of the shindan.
65    pub async fn get_segments_with_title(
66        &self,
67        id: &str,
68        name: &str,
69    ) -> Result<(Segments, String)> {
70        let (title, response_text) = self.submit_shindan(id, name, true).await?;
71        let title = title.context("Title should have been extracted")?;
72        let segments = internal::parse_segments(&response_text)?;
73
74        Ok((segments, title))
75    }
76
77    #[cfg(feature = "html")]
78    /// Get the HTML string of a shindan.
79    pub async fn get_html_str(&self, id: &str, name: &str) -> Result<String> {
80        let (_, response_text) = self.submit_shindan(id, name, false).await?;
81        internal::construct_html_result(id, &response_text, &self.domain.to_string())
82    }
83
84    #[cfg(feature = "html")]
85    /// Get the HTML string of a shindan and the title of the shindan.
86    pub async fn get_html_str_with_title(&self, id: &str, name: &str) -> Result<(String, String)> {
87        let (title, response_text) = self.submit_shindan(id, name, true).await?;
88        let title = title.context("Title should have been extracted")?;
89        let html = internal::construct_html_result(id, &response_text, &self.domain.to_string())?;
90
91        Ok((html, title))
92    }
93
94    // --- Internal Helpers ---
95
96    async fn fetch_document(&self, id: &str) -> Result<Html> {
97        let url = format!("{}{}", self.domain, id);
98        let text = self.client.get(&url).send().await?.text().await?;
99        Ok(Html::parse_document(&text))
100    }
101
102    async fn submit_shindan(
103        &self,
104        id: &str,
105        name: &str,
106        extract_title: bool,
107    ) -> Result<(Option<String>, String)> {
108        let url = format!("{}{}", self.domain, id);
109
110        // 1. Initial GET
111        let initial_response = self.client.get(&url).send().await?;
112        let initial_response_text = initial_response.text().await?;
113
114        let document = Html::parse_document(&initial_response_text);
115
116        // 2. Extract Form Data
117        let form_data = internal::extract_form_data(&document, name)?;
118
119        let title = if extract_title {
120            Some(internal::extract_title(&document)?)
121        } else {
122            None
123        };
124
125        // 3. POST
126        let mut headers = header::HeaderMap::new();
127        headers.insert(
128            header::CONTENT_TYPE,
129            header::HeaderValue::from_static("application/x-www-form-urlencoded"),
130        );
131
132        let post_response = self
133            .client
134            .post(&url)
135            .headers(headers)
136            .form(&form_data)
137            .send()
138            .await?;
139        let response_text = post_response.text().await?;
140
141        Ok((title, response_text))
142    }
143}