use anyhow::{Result, anyhow};
use chrono::{Local, NaiveDate};
use reqwest::blocking::Client;
use serde::Deserialize;
use serde::de::DeserializeOwned;
use serde_json::Value;
const API_BASE: &str = "https://api.aladhan.com/v1";
#[derive(Debug, Clone, Deserialize)]
pub struct PrayerTimings {
#[serde(rename = "Fajr")]
pub fajr: String,
#[serde(rename = "Sunrise")]
pub sunrise: String,
#[serde(rename = "Dhuhr")]
pub dhuhr: String,
#[serde(rename = "Asr")]
pub asr: String,
#[serde(rename = "Sunset")]
pub sunset: String,
#[serde(rename = "Maghrib")]
pub maghrib: String,
#[serde(rename = "Isha")]
pub isha: String,
#[serde(rename = "Imsak")]
pub imsak: String,
#[serde(rename = "Midnight")]
pub midnight: String,
#[serde(rename = "Firstthird")]
pub firstthird: String,
#[serde(rename = "Lastthird")]
pub lastthird: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct HijriMonth {
pub number: i64,
pub en: String,
pub ar: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct WeekdayLocalized {
pub en: String,
pub ar: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct HijriDate {
pub date: String,
pub day: String,
pub month: HijriMonth,
pub year: String,
pub weekday: WeekdayLocalized,
}
#[derive(Debug, Clone, Deserialize)]
pub struct GregorianMonth {
pub number: i64,
pub en: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct GregorianDate {
pub date: String,
pub day: String,
pub month: GregorianMonth,
pub year: String,
pub weekday: WeekdayLocalized,
}
#[derive(Debug, Clone, Deserialize)]
pub struct PrayerDate {
pub readable: String,
pub timestamp: String,
pub hijri: HijriDate,
pub gregorian: GregorianDate,
}
#[derive(Debug, Clone, Deserialize)]
pub struct PrayerMethod {
pub id: i64,
pub name: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct PrayerSchoolObj {
pub id: i64,
pub name: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum PrayerSchool {
Object(PrayerSchoolObj),
Text(String),
}
#[derive(Debug, Clone, Deserialize)]
pub struct PrayerMeta {
pub latitude: f64,
pub longitude: f64,
pub timezone: String,
pub method: PrayerMethod,
pub school: PrayerSchool,
}
#[derive(Debug, Clone, Deserialize)]
pub struct PrayerData {
pub timings: PrayerTimings,
pub date: PrayerDate,
pub meta: PrayerMeta,
}
#[derive(Debug, Clone, Deserialize)]
pub struct NextPrayerData {
pub timings: PrayerTimings,
pub date: PrayerDate,
pub meta: PrayerMeta,
#[serde(rename = "nextPrayer")]
pub next_prayer: String,
#[serde(rename = "nextPrayerTime")]
pub next_prayer_time: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct CalculationMethodParams {
#[serde(rename = "Fajr")]
pub fajr: f64,
#[serde(rename = "Isha")]
pub isha: Value,
}
#[derive(Debug, Clone, Deserialize)]
pub struct CalculationMethod {
pub id: i64,
pub name: String,
pub params: CalculationMethodParams,
}
pub type MethodsResponse = std::collections::HashMap<String, CalculationMethod>;
#[derive(Debug, Clone, Deserialize)]
pub struct QiblaData {
pub latitude: f64,
pub longitude: f64,
pub direction: f64,
}
#[derive(Debug, Deserialize)]
struct ApiEnvelope {
code: i64,
status: String,
data: Value,
}
fn format_date(date: NaiveDate) -> String {
date.format("%d-%m-%Y").to_string()
}
fn today_date() -> NaiveDate {
Local::now().date_naive()
}
fn parse_api_response<T: DeserializeOwned>(payload: Value) -> Result<T> {
let envelope: ApiEnvelope =
serde_json::from_value(payload).map_err(|e| anyhow!("Invalid API response: {e}"))?;
if envelope.code != 200 {
return Err(anyhow!("API {}: {}", envelope.code, envelope.status));
}
if let Some(message) = envelope.data.as_str() {
return Err(anyhow!("API returned message: {message}"));
}
serde_json::from_value(envelope.data).map_err(|e| anyhow!("Invalid API response: {e}"))
}
fn fetch_and_parse<T: DeserializeOwned>(client: &Client, url: &str) -> Result<T> {
let payload: Value = client
.get(url)
.send()
.and_then(|r| r.error_for_status())
.map_err(|e| anyhow!("Network request failed: {e}"))?
.json()
.map_err(|e| anyhow!("Invalid API response: {e}"))?;
parse_api_response(payload)
}
#[derive(Debug, Clone)]
pub struct FetchByCityOptions {
pub city: String,
pub country: String,
pub method: Option<i64>,
pub school: Option<i64>,
pub date: Option<NaiveDate>,
}
pub fn fetch_timings_by_city(client: &Client, opts: &FetchByCityOptions) -> Result<PrayerData> {
let date = format_date(opts.date.unwrap_or_else(today_date));
let mut url = reqwest::Url::parse(&format!("{API_BASE}/timingsByCity/{date}"))?;
{
let mut qp = url.query_pairs_mut();
qp.append_pair("city", &opts.city);
qp.append_pair("country", &opts.country);
if let Some(method) = opts.method {
qp.append_pair("method", &method.to_string());
}
if let Some(school) = opts.school {
qp.append_pair("school", &school.to_string());
}
}
fetch_and_parse(client, url.as_str())
}
#[derive(Debug, Clone)]
pub struct FetchByAddressOptions {
pub address: String,
pub method: Option<i64>,
pub school: Option<i64>,
pub date: Option<NaiveDate>,
}
pub fn fetch_timings_by_address(
client: &Client,
opts: &FetchByAddressOptions,
) -> Result<PrayerData> {
let date = format_date(opts.date.unwrap_or_else(today_date));
let mut url = reqwest::Url::parse(&format!("{API_BASE}/timingsByAddress/{date}"))?;
{
let mut qp = url.query_pairs_mut();
qp.append_pair("address", &opts.address);
if let Some(method) = opts.method {
qp.append_pair("method", &method.to_string());
}
if let Some(school) = opts.school {
qp.append_pair("school", &school.to_string());
}
}
fetch_and_parse(client, url.as_str())
}
#[derive(Debug, Clone)]
pub struct FetchByCoordsOptions {
pub latitude: f64,
pub longitude: f64,
pub method: Option<i64>,
pub school: Option<i64>,
pub timezone: Option<String>,
pub date: Option<NaiveDate>,
}
pub fn fetch_timings_by_coords(client: &Client, opts: &FetchByCoordsOptions) -> Result<PrayerData> {
let date = format_date(opts.date.unwrap_or_else(today_date));
let mut url = reqwest::Url::parse(&format!("{API_BASE}/timings/{date}"))?;
{
let mut qp = url.query_pairs_mut();
qp.append_pair("latitude", &opts.latitude.to_string());
qp.append_pair("longitude", &opts.longitude.to_string());
if let Some(method) = opts.method {
qp.append_pair("method", &method.to_string());
}
if let Some(school) = opts.school {
qp.append_pair("school", &school.to_string());
}
if let Some(timezone) = &opts.timezone {
qp.append_pair("timezonestring", timezone);
}
}
fetch_and_parse(client, url.as_str())
}
#[derive(Debug, Clone)]
pub struct FetchNextPrayerOptions {
pub latitude: f64,
pub longitude: f64,
pub method: Option<i64>,
pub school: Option<i64>,
pub timezone: Option<String>,
}
pub fn fetch_next_prayer(client: &Client, opts: &FetchNextPrayerOptions) -> Result<NextPrayerData> {
let date = format_date(today_date());
let mut url = reqwest::Url::parse(&format!("{API_BASE}/nextPrayer/{date}"))?;
{
let mut qp = url.query_pairs_mut();
qp.append_pair("latitude", &opts.latitude.to_string());
qp.append_pair("longitude", &opts.longitude.to_string());
if let Some(method) = opts.method {
qp.append_pair("method", &method.to_string());
}
if let Some(school) = opts.school {
qp.append_pair("school", &school.to_string());
}
if let Some(timezone) = &opts.timezone {
qp.append_pair("timezonestring", timezone);
}
}
fetch_and_parse(client, url.as_str())
}
#[derive(Debug, Clone)]
pub struct FetchCalendarByCityOptions {
pub city: String,
pub country: String,
pub year: i64,
pub month: Option<i64>,
pub method: Option<i64>,
pub school: Option<i64>,
}
pub fn fetch_calendar_by_city(
client: &Client,
opts: &FetchCalendarByCityOptions,
) -> Result<Vec<PrayerData>> {
let path = match opts.month {
Some(month) => format!("{}/{month}", opts.year),
None => opts.year.to_string(),
};
let mut url = reqwest::Url::parse(&format!("{API_BASE}/calendarByCity/{path}"))?;
{
let mut qp = url.query_pairs_mut();
qp.append_pair("city", &opts.city);
qp.append_pair("country", &opts.country);
if let Some(method) = opts.method {
qp.append_pair("method", &method.to_string());
}
if let Some(school) = opts.school {
qp.append_pair("school", &school.to_string());
}
}
fetch_and_parse(client, url.as_str())
}
#[derive(Debug, Clone)]
pub struct FetchCalendarByAddressOptions {
pub address: String,
pub year: i64,
pub month: Option<i64>,
pub method: Option<i64>,
pub school: Option<i64>,
}
pub fn fetch_calendar_by_address(
client: &Client,
opts: &FetchCalendarByAddressOptions,
) -> Result<Vec<PrayerData>> {
let path = match opts.month {
Some(month) => format!("{}/{month}", opts.year),
None => opts.year.to_string(),
};
let mut url = reqwest::Url::parse(&format!("{API_BASE}/calendarByAddress/{path}"))?;
{
let mut qp = url.query_pairs_mut();
qp.append_pair("address", &opts.address);
if let Some(method) = opts.method {
qp.append_pair("method", &method.to_string());
}
if let Some(school) = opts.school {
qp.append_pair("school", &school.to_string());
}
}
fetch_and_parse(client, url.as_str())
}
#[derive(Debug, Clone)]
pub struct FetchHijriCalendarByAddressOptions {
pub address: String,
pub year: i64,
pub month: i64,
pub method: Option<i64>,
pub school: Option<i64>,
}
pub fn fetch_hijri_calendar_by_address(
client: &Client,
opts: &FetchHijriCalendarByAddressOptions,
) -> Result<Vec<PrayerData>> {
let mut url = reqwest::Url::parse(&format!(
"{API_BASE}/hijriCalendarByAddress/{}/{}",
opts.year, opts.month
))?;
{
let mut qp = url.query_pairs_mut();
qp.append_pair("address", &opts.address);
if let Some(method) = opts.method {
qp.append_pair("method", &method.to_string());
}
if let Some(school) = opts.school {
qp.append_pair("school", &school.to_string());
}
}
fetch_and_parse(client, url.as_str())
}
#[derive(Debug, Clone)]
pub struct FetchHijriCalendarByCityOptions {
pub city: String,
pub country: String,
pub year: i64,
pub month: i64,
pub method: Option<i64>,
pub school: Option<i64>,
}
pub fn fetch_hijri_calendar_by_city(
client: &Client,
opts: &FetchHijriCalendarByCityOptions,
) -> Result<Vec<PrayerData>> {
let mut url = reqwest::Url::parse(&format!(
"{API_BASE}/hijriCalendarByCity/{}/{}",
opts.year, opts.month
))?;
{
let mut qp = url.query_pairs_mut();
qp.append_pair("city", &opts.city);
qp.append_pair("country", &opts.country);
if let Some(method) = opts.method {
qp.append_pair("method", &method.to_string());
}
if let Some(school) = opts.school {
qp.append_pair("school", &school.to_string());
}
}
fetch_and_parse(client, url.as_str())
}
pub fn fetch_methods(client: &Client) -> Result<MethodsResponse> {
fetch_and_parse(client, &format!("{API_BASE}/methods"))
}
pub fn fetch_qibla(client: &Client, latitude: f64, longitude: f64) -> Result<QiblaData> {
fetch_and_parse(client, &format!("{API_BASE}/qibla/{latitude}/{longitude}"))
}