1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
use chrono::{NaiveDate, Utc};
use eyre::Result;
use random::Source;
use serde::Deserialize;


#[derive(Debug)]
pub struct Comic {
    pub date: NaiveDate,
    #[allow(dead_code)]
    pub num: u32,
    pub img: String,
    pub link: String,
    pub title: String,
    pub transcript: String,
}


impl Comic {
    pub async fn fetch(num: u32) -> Result<Self> {
        let comic = fetch_comic(Some(num)).await?;
        Ok(comic.into())
    }

    pub async fn latest() -> Result<Self> {
        let comic = fetch_comic(None).await?;
        Ok(comic.into())
    }

    pub async fn random() -> Result<Self> {
        let mut rnd = random::default(Utc::now().timestamp_millis() as u64);
        let latest = fetch_comic(None).await?;
        let num = (rnd.read::<u32>() % latest.num) + 1;
        let comic = fetch_comic(Some(num)).await?;
        Ok(comic.into())
    }

    pub fn open(&self) -> Result<()> {
        webbrowser::open(&self.link)?;
        Ok(())
    }
}


impl From<RawComic> for Comic {
    fn from(value: RawComic) -> Self {

        let title = match &value.title {
            title if title.is_empty() => value.safe_title.to_owned(),
            title => title.to_owned(),
        };

        let transcript = match &value.transcript {
            transcript if transcript.is_empty() => value.alt.to_owned(),
            transcript => transcript.to_owned(),
        };

        let link = match &value.link {
            link if link.is_empty() => format!("https://xkcd.com/{}", value.num),
            link => link.to_owned(),
        };

        Self {
            title, transcript, link,
            date: value.date().unwrap_or(NaiveDate::MIN),
            num: value.num,
            img: value.img,
        }
    }
}


#[derive(Debug, Deserialize)]
struct RawComic {
    month: String,
    num: u32,
    link: String,
    year: String,
    safe_title: String,
    transcript: String,
    alt: String,
    img: String,
    title: String,
    day: String,
}

impl RawComic {
    fn date(&self) -> Option<NaiveDate> {
        NaiveDate::from_ymd_opt(
            self.year.parse().ok()?,
            self.month.parse().ok()?,
            self.day.parse().ok()?,
        )
    }
}


async fn fetch_comic(num: Option<u32>) -> Result<RawComic> {
    let url = match num {
        Some(num) => format!("https://xkcd.com/{}/info.0.json", num),
        None => "https://xkcd.com/info.0.json".to_string(),
    };
    let resp = reqwest::get(url).await?;
    let comic: RawComic = resp.json().await?;
    Ok(comic)
}