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#[derive(Clone, Debug)]
13pub struct ShindanClient {
14 client: Client,
15 domain: ShindanDomain,
16}
17
18impl ShindanClient {
19 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 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 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 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 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 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 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 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 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 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 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 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}