use crate::bing;
use crate::bing::Market;
use crate::date;
use anyhow::anyhow;
use chrono::{DateTime, NaiveDate, NaiveDateTime, Utc};
use regex::Regex;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
use std::fmt::{Display, Formatter};
use std::sync::LazyLock;
use url::Url;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Image {
pub url: Url,
#[serde(with = "date::ymd")]
pub start_date: NaiveDate,
#[serde(with = "date::ymdhm")]
pub full_start_date: DateTime<Utc>,
#[serde(with = "date::ymd")]
pub end_date: NaiveDate,
pub id: String,
#[serde(skip)]
pub id_parsed: Option<ID>,
pub copyright: String,
#[serde(skip)]
pub copyright_parsed: Option<Copyright>,
pub copyright_link: Url,
pub title: String,
pub quiz_link: Url,
pub wallpaper: bool,
pub hash: String,
}
impl Image {
pub fn parse(image: bing::Image) -> Result<Self, anyhow::Error> {
let bing::Image {
start_date,
full_start_date,
end_date,
url,
copyright,
copyright_link,
title,
quiz_link,
wallpaper,
hash,
..
} = image;
let base = Url::parse(bing::BASE_URL)?;
let url = base.join(&url)?;
let id = url
.query_pairs()
.find_map(|(key, id)| {
if key == "id" {
Some(id.into_owned())
} else {
None
}
})
.ok_or_else(|| anyhow!("missing id"))?;
Ok(Image {
url,
start_date: NaiveDate::parse_from_str(&start_date, "%Y%m%d")?,
full_start_date: NaiveDateTime::parse_from_str(&full_start_date, "%Y%m%d%H%M")?
.and_utc(),
end_date: NaiveDate::parse_from_str(&end_date, "%Y%m%d")?,
id_parsed: ID::parse(&id),
id,
copyright_parsed: Copyright::parse(©right),
copyright_link: base.join(©right_link)?,
copyright,
title,
quiz_link: base.join(&quiz_link)?,
wallpaper,
hash,
})
}
pub fn url_builder(&self) -> bing::UrlBuilder {
bing::UrlBuilder::new(&self.id)
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Copyright {
pub description: String,
pub copyright: String,
}
static COPYRIGHT_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(
r"(?x)
(?P<description>.*?)
\s*\(
(?P<copyright>.*?)
\)
",
)
.unwrap()
});
impl Copyright {
pub fn parse(s: impl AsRef<str>) -> Option<Self> {
if let Some(captures) = COPYRIGHT_REGEX.captures(s.as_ref())
&& let (Some(description), Some(copyright)) =
(captures.name("description"), captures.name("copyright"))
{
Some(Copyright {
description: description.as_str().to_owned(),
copyright: copyright.as_str().to_owned(),
})
} else {
None
}
}
}
#[skip_serializing_none]
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct ID {
pub name: String,
pub market: Option<Market>,
pub number: usize,
pub uhd: bool,
pub width: Option<usize>,
pub height: Option<usize>,
pub extension: String,
}
impl Display for ID {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let ID {
name,
market,
number,
uhd,
width,
height,
extension,
} = self;
let market = if let Some(market) = market {
market.code().to_ascii_uppercase()
} else {
"ROW".to_owned()
};
if *uhd {
return write!(f, "OHR.{name}_{market}{number}_UHD.{extension}");
}
if let (Some(width), Some(height)) = (width, height) {
write!(
f,
"OHR.{name}_{market}{number}_{width}x{height}.{extension}"
)
} else {
Err(std::fmt::Error)
}
}
}
static ID_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(
r"(?x)
^OHR
\.
(?P<name>\w+)
_
(?P<market>ROW|\w{2}-\w{2})
(?P<number>\d+)
_
(
(?P<width>\d+)x(?P<height>\d+)
|
(?P<uhd>UHD)
)
\.
(?P<extension>\w+)$",
)
.unwrap()
});
impl ID {
pub fn parse(id: impl AsRef<str>) -> Option<Self> {
let captures = ID_REGEX.captures(id.as_ref())?;
let uhd = captures.name("uhd").is_some();
let id = Self {
name: captures.name("name")?.as_str().to_owned(),
market: captures.name("market")?.as_str().parse::<Market>().ok(),
number: captures.name("number")?.as_str().parse::<usize>().ok()?,
uhd,
width: if uhd {
None
} else {
Some(captures.name("width")?.as_str().parse::<usize>().ok()?)
},
height: if uhd {
None
} else {
Some(captures.name("height")?.as_str().parse::<usize>().ok()?)
},
extension: captures.name("extension")?.as_str().to_owned(),
};
Some(id)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bing::Market;
#[test]
fn test_id() {
let test_cases = vec![
(
"OHR.YosemiteFirefall_ROW8895162487_1920x1080.jpg",
ID {
name: "YosemiteFirefall".to_string(),
market: None,
number: 8895162487,
width: Some(1920),
height: Some(1080),
extension: "jpg".to_string(),
..Default::default()
},
),
(
"OHR.HalfDomeYosemite_EN-US4890007214_UHD.jpg",
ID {
name: "HalfDomeYosemite".to_string(),
market: Some(Market::EN_US),
number: 4890007214,
uhd: true,
extension: "jpg".to_string(),
..Default::default()
},
),
];
for (id, expected) in test_cases {
let parsed = ID::parse(id).expect("failed to parse id");
assert_eq!(parsed, expected);
assert_eq!(parsed.to_string(), id);
}
}
}