1use anyhow::{Result, anyhow};
2use chrono::{Local, NaiveDate};
3use reqwest::blocking::Client;
4use serde::Deserialize;
5use serde::de::DeserializeOwned;
6use serde_json::Value;
7
8const API_BASE: &str = "https://api.aladhan.com/v1";
9
10#[derive(Debug, Clone, Deserialize)]
11pub struct PrayerTimings {
12 #[serde(rename = "Fajr")]
13 pub fajr: String,
14 #[serde(rename = "Sunrise")]
15 pub sunrise: String,
16 #[serde(rename = "Dhuhr")]
17 pub dhuhr: String,
18 #[serde(rename = "Asr")]
19 pub asr: String,
20 #[serde(rename = "Sunset")]
21 pub sunset: String,
22 #[serde(rename = "Maghrib")]
23 pub maghrib: String,
24 #[serde(rename = "Isha")]
25 pub isha: String,
26 #[serde(rename = "Imsak")]
27 pub imsak: String,
28 #[serde(rename = "Midnight")]
29 pub midnight: String,
30 #[serde(rename = "Firstthird")]
31 pub firstthird: String,
32 #[serde(rename = "Lastthird")]
33 pub lastthird: String,
34}
35
36#[derive(Debug, Clone, Deserialize)]
37pub struct HijriMonth {
38 pub number: i64,
39 pub en: String,
40 pub ar: String,
41}
42
43#[derive(Debug, Clone, Deserialize)]
44pub struct WeekdayLocalized {
45 pub en: String,
46 pub ar: Option<String>,
47}
48
49#[derive(Debug, Clone, Deserialize)]
50pub struct HijriDate {
51 pub date: String,
52 pub day: String,
53 pub month: HijriMonth,
54 pub year: String,
55 pub weekday: WeekdayLocalized,
56}
57
58#[derive(Debug, Clone, Deserialize)]
59pub struct GregorianMonth {
60 pub number: i64,
61 pub en: String,
62}
63
64#[derive(Debug, Clone, Deserialize)]
65pub struct GregorianDate {
66 pub date: String,
67 pub day: String,
68 pub month: GregorianMonth,
69 pub year: String,
70 pub weekday: WeekdayLocalized,
71}
72
73#[derive(Debug, Clone, Deserialize)]
74pub struct PrayerDate {
75 pub readable: String,
76 pub timestamp: String,
77 pub hijri: HijriDate,
78 pub gregorian: GregorianDate,
79}
80
81#[derive(Debug, Clone, Deserialize)]
82pub struct PrayerMethod {
83 pub id: i64,
84 pub name: String,
85}
86
87#[derive(Debug, Clone, Deserialize)]
88pub struct PrayerSchoolObj {
89 pub id: i64,
90 pub name: String,
91}
92
93#[derive(Debug, Clone, Deserialize)]
94#[serde(untagged)]
95pub enum PrayerSchool {
96 Object(PrayerSchoolObj),
97 Text(String),
98}
99
100#[derive(Debug, Clone, Deserialize)]
101pub struct PrayerMeta {
102 pub latitude: f64,
103 pub longitude: f64,
104 pub timezone: String,
105 pub method: PrayerMethod,
106 pub school: PrayerSchool,
107}
108
109#[derive(Debug, Clone, Deserialize)]
110pub struct PrayerData {
111 pub timings: PrayerTimings,
112 pub date: PrayerDate,
113 pub meta: PrayerMeta,
114}
115
116#[derive(Debug, Clone, Deserialize)]
117pub struct NextPrayerData {
118 pub timings: PrayerTimings,
119 pub date: PrayerDate,
120 pub meta: PrayerMeta,
121 #[serde(rename = "nextPrayer")]
122 pub next_prayer: String,
123 #[serde(rename = "nextPrayerTime")]
124 pub next_prayer_time: String,
125}
126
127#[derive(Debug, Clone, Deserialize)]
128pub struct CalculationMethodParams {
129 #[serde(rename = "Fajr")]
130 pub fajr: f64,
131 #[serde(rename = "Isha")]
132 pub isha: Value,
133}
134
135#[derive(Debug, Clone, Deserialize)]
136pub struct CalculationMethod {
137 pub id: i64,
138 pub name: String,
139 pub params: CalculationMethodParams,
140}
141
142pub type MethodsResponse = std::collections::HashMap<String, CalculationMethod>;
143
144#[derive(Debug, Clone, Deserialize)]
145pub struct QiblaData {
146 pub latitude: f64,
147 pub longitude: f64,
148 pub direction: f64,
149}
150
151#[derive(Debug, Deserialize)]
152struct ApiEnvelope {
153 code: i64,
154 status: String,
155 data: Value,
156}
157
158fn format_date(date: NaiveDate) -> String {
159 date.format("%d-%m-%Y").to_string()
160}
161
162fn today_date() -> NaiveDate {
163 Local::now().date_naive()
164}
165
166fn parse_api_response<T: DeserializeOwned>(payload: Value) -> Result<T> {
167 let envelope: ApiEnvelope =
168 serde_json::from_value(payload).map_err(|e| anyhow!("Invalid API response: {e}"))?;
169
170 if envelope.code != 200 {
171 return Err(anyhow!("API {}: {}", envelope.code, envelope.status));
172 }
173
174 if let Some(message) = envelope.data.as_str() {
175 return Err(anyhow!("API returned message: {message}"));
176 }
177
178 serde_json::from_value(envelope.data).map_err(|e| anyhow!("Invalid API response: {e}"))
179}
180
181fn fetch_and_parse<T: DeserializeOwned>(client: &Client, url: &str) -> Result<T> {
182 let payload: Value = client
183 .get(url)
184 .send()
185 .and_then(|r| r.error_for_status())
186 .map_err(|e| anyhow!("Network request failed: {e}"))?
187 .json()
188 .map_err(|e| anyhow!("Invalid API response: {e}"))?;
189
190 parse_api_response(payload)
191}
192
193#[derive(Debug, Clone)]
194pub struct FetchByCityOptions {
195 pub city: String,
196 pub country: String,
197 pub method: Option<i64>,
198 pub school: Option<i64>,
199 pub date: Option<NaiveDate>,
200}
201
202pub fn fetch_timings_by_city(client: &Client, opts: &FetchByCityOptions) -> Result<PrayerData> {
203 let date = format_date(opts.date.unwrap_or_else(today_date));
204 let mut url = reqwest::Url::parse(&format!("{API_BASE}/timingsByCity/{date}"))?;
205 {
206 let mut qp = url.query_pairs_mut();
207 qp.append_pair("city", &opts.city);
208 qp.append_pair("country", &opts.country);
209 if let Some(method) = opts.method {
210 qp.append_pair("method", &method.to_string());
211 }
212 if let Some(school) = opts.school {
213 qp.append_pair("school", &school.to_string());
214 }
215 }
216
217 fetch_and_parse(client, url.as_str())
218}
219
220#[derive(Debug, Clone)]
221pub struct FetchByAddressOptions {
222 pub address: String,
223 pub method: Option<i64>,
224 pub school: Option<i64>,
225 pub date: Option<NaiveDate>,
226}
227
228pub fn fetch_timings_by_address(
229 client: &Client,
230 opts: &FetchByAddressOptions,
231) -> Result<PrayerData> {
232 let date = format_date(opts.date.unwrap_or_else(today_date));
233 let mut url = reqwest::Url::parse(&format!("{API_BASE}/timingsByAddress/{date}"))?;
234 {
235 let mut qp = url.query_pairs_mut();
236 qp.append_pair("address", &opts.address);
237 if let Some(method) = opts.method {
238 qp.append_pair("method", &method.to_string());
239 }
240 if let Some(school) = opts.school {
241 qp.append_pair("school", &school.to_string());
242 }
243 }
244
245 fetch_and_parse(client, url.as_str())
246}
247
248#[derive(Debug, Clone)]
249pub struct FetchByCoordsOptions {
250 pub latitude: f64,
251 pub longitude: f64,
252 pub method: Option<i64>,
253 pub school: Option<i64>,
254 pub timezone: Option<String>,
255 pub date: Option<NaiveDate>,
256}
257
258pub fn fetch_timings_by_coords(client: &Client, opts: &FetchByCoordsOptions) -> Result<PrayerData> {
259 let date = format_date(opts.date.unwrap_or_else(today_date));
260 let mut url = reqwest::Url::parse(&format!("{API_BASE}/timings/{date}"))?;
261 {
262 let mut qp = url.query_pairs_mut();
263 qp.append_pair("latitude", &opts.latitude.to_string());
264 qp.append_pair("longitude", &opts.longitude.to_string());
265 if let Some(method) = opts.method {
266 qp.append_pair("method", &method.to_string());
267 }
268 if let Some(school) = opts.school {
269 qp.append_pair("school", &school.to_string());
270 }
271 if let Some(timezone) = &opts.timezone {
272 qp.append_pair("timezonestring", timezone);
273 }
274 }
275
276 fetch_and_parse(client, url.as_str())
277}
278
279#[derive(Debug, Clone)]
280pub struct FetchNextPrayerOptions {
281 pub latitude: f64,
282 pub longitude: f64,
283 pub method: Option<i64>,
284 pub school: Option<i64>,
285 pub timezone: Option<String>,
286}
287
288pub fn fetch_next_prayer(client: &Client, opts: &FetchNextPrayerOptions) -> Result<NextPrayerData> {
289 let date = format_date(today_date());
290 let mut url = reqwest::Url::parse(&format!("{API_BASE}/nextPrayer/{date}"))?;
291 {
292 let mut qp = url.query_pairs_mut();
293 qp.append_pair("latitude", &opts.latitude.to_string());
294 qp.append_pair("longitude", &opts.longitude.to_string());
295 if let Some(method) = opts.method {
296 qp.append_pair("method", &method.to_string());
297 }
298 if let Some(school) = opts.school {
299 qp.append_pair("school", &school.to_string());
300 }
301 if let Some(timezone) = &opts.timezone {
302 qp.append_pair("timezonestring", timezone);
303 }
304 }
305
306 fetch_and_parse(client, url.as_str())
307}
308
309#[derive(Debug, Clone)]
310pub struct FetchCalendarByCityOptions {
311 pub city: String,
312 pub country: String,
313 pub year: i64,
314 pub month: Option<i64>,
315 pub method: Option<i64>,
316 pub school: Option<i64>,
317}
318
319pub fn fetch_calendar_by_city(
320 client: &Client,
321 opts: &FetchCalendarByCityOptions,
322) -> Result<Vec<PrayerData>> {
323 let path = match opts.month {
324 Some(month) => format!("{}/{month}", opts.year),
325 None => opts.year.to_string(),
326 };
327 let mut url = reqwest::Url::parse(&format!("{API_BASE}/calendarByCity/{path}"))?;
328 {
329 let mut qp = url.query_pairs_mut();
330 qp.append_pair("city", &opts.city);
331 qp.append_pair("country", &opts.country);
332 if let Some(method) = opts.method {
333 qp.append_pair("method", &method.to_string());
334 }
335 if let Some(school) = opts.school {
336 qp.append_pair("school", &school.to_string());
337 }
338 }
339
340 fetch_and_parse(client, url.as_str())
341}
342
343#[derive(Debug, Clone)]
344pub struct FetchCalendarByAddressOptions {
345 pub address: String,
346 pub year: i64,
347 pub month: Option<i64>,
348 pub method: Option<i64>,
349 pub school: Option<i64>,
350}
351
352pub fn fetch_calendar_by_address(
353 client: &Client,
354 opts: &FetchCalendarByAddressOptions,
355) -> Result<Vec<PrayerData>> {
356 let path = match opts.month {
357 Some(month) => format!("{}/{month}", opts.year),
358 None => opts.year.to_string(),
359 };
360 let mut url = reqwest::Url::parse(&format!("{API_BASE}/calendarByAddress/{path}"))?;
361 {
362 let mut qp = url.query_pairs_mut();
363 qp.append_pair("address", &opts.address);
364 if let Some(method) = opts.method {
365 qp.append_pair("method", &method.to_string());
366 }
367 if let Some(school) = opts.school {
368 qp.append_pair("school", &school.to_string());
369 }
370 }
371
372 fetch_and_parse(client, url.as_str())
373}
374
375#[derive(Debug, Clone)]
376pub struct FetchHijriCalendarByAddressOptions {
377 pub address: String,
378 pub year: i64,
379 pub month: i64,
380 pub method: Option<i64>,
381 pub school: Option<i64>,
382}
383
384pub fn fetch_hijri_calendar_by_address(
385 client: &Client,
386 opts: &FetchHijriCalendarByAddressOptions,
387) -> Result<Vec<PrayerData>> {
388 let mut url = reqwest::Url::parse(&format!(
389 "{API_BASE}/hijriCalendarByAddress/{}/{}",
390 opts.year, opts.month
391 ))?;
392 {
393 let mut qp = url.query_pairs_mut();
394 qp.append_pair("address", &opts.address);
395 if let Some(method) = opts.method {
396 qp.append_pair("method", &method.to_string());
397 }
398 if let Some(school) = opts.school {
399 qp.append_pair("school", &school.to_string());
400 }
401 }
402
403 fetch_and_parse(client, url.as_str())
404}
405
406#[derive(Debug, Clone)]
407pub struct FetchHijriCalendarByCityOptions {
408 pub city: String,
409 pub country: String,
410 pub year: i64,
411 pub month: i64,
412 pub method: Option<i64>,
413 pub school: Option<i64>,
414}
415
416pub fn fetch_hijri_calendar_by_city(
417 client: &Client,
418 opts: &FetchHijriCalendarByCityOptions,
419) -> Result<Vec<PrayerData>> {
420 let mut url = reqwest::Url::parse(&format!(
421 "{API_BASE}/hijriCalendarByCity/{}/{}",
422 opts.year, opts.month
423 ))?;
424 {
425 let mut qp = url.query_pairs_mut();
426 qp.append_pair("city", &opts.city);
427 qp.append_pair("country", &opts.country);
428 if let Some(method) = opts.method {
429 qp.append_pair("method", &method.to_string());
430 }
431 if let Some(school) = opts.school {
432 qp.append_pair("school", &school.to_string());
433 }
434 }
435
436 fetch_and_parse(client, url.as_str())
437}
438
439pub fn fetch_methods(client: &Client) -> Result<MethodsResponse> {
440 fetch_and_parse(client, &format!("{API_BASE}/methods"))
441}
442
443pub fn fetch_qibla(client: &Client, latitude: f64, longitude: f64) -> Result<QiblaData> {
444 fetch_and_parse(client, &format!("{API_BASE}/qibla/{latitude}/{longitude}"))
445}