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}