bangumi_data/
lib.rs

1use std::{collections::BTreeMap, fmt::Display, marker::PhantomData, str::FromStr};
2
3use iso8601::DateTime;
4use nom::{IResult, Parser, branch::alt, bytes::complete::tag, combinator::map};
5use serde::{Deserialize, Deserializer, Serialize, de::Visitor};
6
7const ROOT: &str = "https://github.com/bangumi-data/bangumi-data/raw/master";
8
9#[cfg(feature = "reqwest")]
10pub async fn get_all() -> Result<BangumiData, reqwest::Error> {
11    reqwest::get(format!("{ROOT}/dist/data.json"))
12        .await?
13        .json()
14        .await
15}
16
17#[cfg(feature = "reqwest")]
18pub async fn get_by_month(year: u32, month: u8) -> Result<Vec<Item>, reqwest::Error> {
19    assert!(month <= 12);
20    assert!(year >= 1960);
21
22    reqwest::get(format!("{ROOT}/data/items/{year}/{month:02}.json"))
23        .await?
24        .json()
25        .await
26}
27
28#[cfg(feature = "reqwest")]
29pub async fn get_info_site() -> Result<BTreeMap<String, SiteMeta>, reqwest::Error> {
30    reqwest::get(format!("{ROOT}/data/sites/info.json"))
31        .await?
32        .json()
33        .await
34}
35
36#[cfg(feature = "reqwest")]
37pub async fn get_on_air_site() -> Result<BTreeMap<String, SiteMeta>, reqwest::Error> {
38    reqwest::get(format!("{ROOT}/data/sites/onair.json"))
39        .await?
40        .json()
41        .await
42}
43
44#[cfg(feature = "reqwest")]
45pub async fn get_resource_site() -> Result<BTreeMap<String, SiteMeta>, reqwest::Error> {
46    reqwest::get(format!("{ROOT}/data/sites/resource.json"))
47        .await?
48        .json()
49        .await
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
53#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
54#[serde(rename_all = "camelCase")]
55pub struct BangumiData {
56    pub site_meta: BTreeMap<String, SiteMeta>,
57    pub items: Vec<Item>,
58}
59
60impl FromStr for BangumiData {
61    type Err = serde_json::Error;
62
63    fn from_str(s: &str) -> Result<Self, Self::Err> {
64        serde_json::from_str(s)
65    }
66}
67
68impl BangumiData {
69    pub fn from_bytes(s: &[u8]) -> Result<Self, serde_json::Error> {
70        serde_json::from_slice(s)
71    }
72}
73
74#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
75#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
76#[serde(rename_all = "camelCase")]
77pub struct SiteMeta {
78    pub title: String,
79    pub url_template: String,
80    #[serde(rename = "type", deserialize_with = "empty_str", default)]
81    pub site_type: Option<String>,
82    pub regions: Option<Vec<String>>,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
86#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
87#[serde(rename_all = "lowercase")]
88#[non_exhaustive]
89pub enum ItemType {
90    TV,
91    Web,
92    Ova,
93    Movie,
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
97#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
98#[serde(rename_all = "camelCase")]
99pub struct Item {
100    pub title: String,
101    pub title_translate: BTreeMap<Language, Vec<String>>,
102    #[serde(rename = "type")]
103    pub item_type: ItemType,
104    pub lang: Language,
105    pub official_site: String,
106    #[serde(deserialize_with = "empty_str", default)]
107    #[cfg_attr(feature = "ts", ts(type = "Option<String>"))]
108    pub begin: Option<DateTime>,
109    #[serde(deserialize_with = "empty_str", default)]
110    #[cfg_attr(feature = "ts", ts(type = "Option<String>"))]
111    pub end: Option<DateTime>,
112    pub sites: Vec<Site>,
113    #[serde(deserialize_with = "empty_str", default)]
114    pub broadcast: Option<Broadcast>,
115    #[serde(deserialize_with = "empty_str", default)]
116    pub comment: Option<String>,
117}
118
119#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
120#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
121#[serde(rename_all = "camelCase")]
122pub struct Site {
123    pub site: String,
124    #[serde(deserialize_with = "empty_str", default)]
125    pub id: Option<String>,
126    #[serde(deserialize_with = "empty_str", default)]
127    pub begin: Option<String>,
128    #[serde(deserialize_with = "empty_str", default)]
129    pub broadcast: Option<String>,
130    #[serde(deserialize_with = "empty_str", default)]
131    pub comment: Option<String>,
132    #[serde(deserialize_with = "empty_str", default)]
133    pub url: Option<String>,
134    pub regions: Option<Vec<String>>,
135}
136
137#[derive(Debug, Clone, PartialEq, Eq)]
138#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
139pub struct Broadcast {
140    #[cfg_attr(feature = "ts", ts(type = "String"))]
141    pub begin: DateTime,
142    #[cfg_attr(feature = "ts", ts(type = "String"))]
143    pub period: Period,
144}
145
146impl FromStr for Broadcast {
147    type Err = String;
148
149    fn from_str(s: &str) -> Result<Self, Self::Err> {
150        parse(s.as_bytes())
151            .map_err(|e| format!("Unable to parse broadcast: {e}"))
152            .map(|(_, b)| b)
153    }
154}
155
156impl<'de> Deserialize<'de> for Broadcast {
157    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
158        let s = String::deserialize(deserializer)?;
159        Broadcast::from_str(&s).map_err(serde::de::Error::custom)
160    }
161}
162
163impl Display for Broadcast {
164    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
165        let period = match self.period {
166            Period::Once => "0D",
167            Period::Daily => "1D",
168            Period::BiDaily => "2D",
169            Period::Weekly => "7D",
170            Period::Monthly => "1M",
171        };
172        write!(f, "R/{}/P{}", self.begin, period)
173    }
174}
175
176impl Serialize for Broadcast {
177    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
178        serializer.collect_str(self)
179    }
180}
181
182#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
183#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
184pub enum Period {
185    Once,
186    Daily,
187    BiDaily,
188    Weekly,
189    Monthly,
190}
191
192fn empty_str<'de, T, D>(de: D) -> Result<Option<T>, D::Error>
193where
194    T: FromStr + 'de,
195    T::Err: Display,
196    D: Deserializer<'de>,
197{
198    struct EmptyStringVisitor<'de, T>(PhantomData<&'de T>);
199
200    impl<'de, T> Visitor<'de> for EmptyStringVisitor<'de, T>
201    where
202        T: FromStr,
203        T::Err: Display,
204    {
205        type Value = Option<T>;
206
207        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
208            formatter.write_str("a string")
209        }
210
211        fn visit_unit<E>(self) -> Result<Self::Value, E>
212        where
213            E: serde::de::Error,
214        {
215            Ok(None)
216        }
217
218        fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
219        where
220            E: serde::de::Error,
221        {
222            if v.is_empty() {
223                return Ok(None);
224            }
225            T::from_str(v).map_err(serde::de::Error::custom).map(Some)
226        }
227    }
228
229    de.deserialize_any(EmptyStringVisitor(PhantomData))
230}
231
232fn parse_period(input: &[u8]) -> IResult<&[u8], Period> {
233    alt((
234        map(tag("0D"), |_| Period::Once),
235        map(tag("1D"), |_| Period::Daily),
236        map(tag("2D"), |_| Period::BiDaily),
237        map(tag("7D"), |_| Period::Weekly),
238        map(tag("1M"), |_| Period::Monthly),
239    ))
240    .parse(input)
241}
242
243fn parse(input: &[u8]) -> IResult<&[u8], Broadcast> {
244    map(
245        (
246            tag("R/"),
247            iso8601::parsers::parse_datetime,
248            tag("/P"),
249            parse_period,
250        ),
251        |(_, begin, _, period)| Broadcast { begin, period },
252    )
253    .parse(input)
254}
255
256#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
257#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
258#[non_exhaustive]
259pub enum Language {
260    #[serde(rename = "zh-Hans")]
261    ZhHans,
262    #[serde(rename = "zh-Hant")]
263    ZhHant,
264    #[serde(rename = "en")]
265    En,
266    #[serde(rename = "ja")]
267    Ja,
268}
269
270#[cfg(test)]
271mod test {
272    use super::*;
273
274    #[test]
275    fn test_broadcast() {
276        const BYTES: &[u8] = b"R/2020-01-01T13:00:00Z/P0D";
277        let (_, a) = parse(BYTES).unwrap();
278        assert_eq!(a.period, Period::Once);
279
280        let a_str = a.to_string();
281        println!("{}", a_str);
282        assert_eq!(BYTES, a_str.as_bytes());
283        let (_, b) = parse(a_str.as_bytes()).unwrap();
284
285        assert_eq!(a, b);
286    }
287
288    #[test]
289    fn test_datetime() {
290        let time = iso8601::datetime("2020-01-01T13:00:00Z").unwrap();
291        let (_, b) = parse(b"R/2020-01-01T13:00:00Z/P0D").unwrap();
292        assert_eq!(b.period, Period::Once);
293        assert_eq!(b.begin, time);
294        let (_, b) = parse(b"R/2020-01-01T13:00:00Z/P1D").unwrap();
295        assert_eq!(b.period, Period::Daily);
296        let (_, b) = parse(b"R/2020-01-01T13:00:00Z/P7D").unwrap();
297        assert_eq!(b.period, Period::Weekly);
298        let (_, b) = parse(b"R/2020-01-01T13:00:00Z/P1M").unwrap();
299        assert_eq!(b.period, Period::Monthly);
300    }
301
302    #[test]
303    fn local() {
304        let s = std::fs::read_to_string("data/dist.json").unwrap();
305        let b = BangumiData::from_str(&s).unwrap();
306        println!("{b:#?}",)
307    }
308
309    #[tokio::test]
310    async fn remote() {
311        println!("{:#?}\n============", get_all().await.unwrap());
312        println!("{:#?}\n============", get_by_month(2023, 10).await.unwrap());
313        println!("{:#?}\n============", get_info_site().await.unwrap());
314        println!("{:#?}\n============", get_on_air_site().await.unwrap());
315        println!("{:#?}\n============", get_resource_site().await.unwrap());
316    }
317}