1use crate::equity::Equity;
2use crate::utils::formatter::year_url;
3use crate::utils::structs::HistoricalData;
4use crate::utils::yahoo::ApiResponse;
5use reqwest::Client;
6use serde_json::Value;
7use std::error::Error;
8use std::time::{Duration, Instant};
9#[derive(Debug)]
11pub struct YahooFinanceClient {
12 client: Client,
13 crumb: Option<String>,
14 last_refresh: Option<Instant>,
15}
16#[derive(Debug)]
17pub struct InvalidRequest;
18
19impl YahooFinanceClient {
20 pub async fn new() -> Result<Self, Box<dyn Error>> {
21 let client = Client::builder()
22 .cookie_store(true)
23 .https_only(true)
24 .user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
25 .build()?;
26
27 Ok(YahooFinanceClient {
28 client,
29 crumb: None,
30 last_refresh: None,
31 })
32 }
33
34 async fn refresh_crumb(&mut self) -> Result<(), Box<dyn Error>> {
35 self.client.get("https://fc.yahoo.com").send().await?;
36
37 let crumb = self
38 .client
39 .get("https://query1.finance.yahoo.com/v1/test/getcrumb")
40 .send()
41 .await?
42 .text()
43 .await?;
44
45 self.crumb = Some(crumb);
46 self.last_refresh = Some(Instant::now());
47 Ok(())
48 }
49
50 async fn ensure_crumb_valid(&mut self) -> Result<(), Box<dyn Error>> {
52 let crumb_ttl = Duration::from_secs(15 * 60); match (self.crumb.as_ref(), self.last_refresh) {
54 (Some(_), Some(t)) if t.elapsed() < crumb_ttl => Ok(()),
55 _ => self.refresh_crumb().await,
56 }
57 }
58 pub async fn fetch_historical(&mut self, ticker: &str) -> Result<Equity, Box<dyn Error>> {
60 let request = self.crumbed_request(&year_url(ticker)).await?;
61 let historical_data = HistoricalData::new(&request);
62 Ok(Equity::new(ticker.to_owned(), historical_data))
63 }
64 pub async fn fetch_quote_summary(
74 &mut self,
75 symbol: &str,
76 ) -> Result<ApiResponse, Box<dyn Error>> {
77 self.ensure_crumb_valid().await?;
78
79 let crumb = self.crumb.as_ref().ok_or("Crumb not found")?;
80 let url = format!(
81 "https://query1.finance.yahoo.com/v10/finance/quoteSummary/{}?modules=assetProfile%2CfinancialData&crumb={}",
82 symbol, crumb
83 );
84
85 let response = self.crumbed_request(&url).await?;
86 print!("{:#?}", response);
87 Ok(serde_json::from_value::<ApiResponse>(response)?)
88 }
89
90 async fn crumbed_request(&mut self, url: &str) -> Result<Value, Box<dyn Error>> {
91 self.ensure_crumb_valid().await?;
92 let crumb = self.crumb.as_ref().ok_or("Crumb not found")?;
93 let full_url = format!("{}&crumb={}", url, crumb);
94
95 let response = self
96 .client
97 .get(&full_url)
98 .header("Accept", "application/json")
99 .send()
100 .await?;
101
102 let text = response.text().await?;
103 Ok(serde_json::from_str(&text)?)
104 }
105}