Skip to main content

radiko_rs/models/
program.rs

1use chrono::{DateTime, NaiveDateTime, TimeZone, Utc};
2use chrono_tz::{Asia::Tokyo, Tz};
3use serde_derive::{Deserialize, Serialize};
4
5use crate::dto::program_xml::{ProgramXml, RadikoProgramXml};
6
7// ```json
8// "data": [
9//   {
10//     "start_time": "2025-06-29 00:00:00",
11//     "end_time": "2025-06-29 01:30:00",
12//     "start_time_s": "2400",
13//     "end_time_s": "2530",
14//     "program_date": "20250628",
15//     "program_url": "https://www.mbs1179.com/yaru/",
16//     "station_id": "MBS",
17//     "performer": "極楽とんぼ、河合郁人、さらば青春の光(週替わり)、トム・ブラウン(週替わり)、小沢一敬(週替わり)(スピードワゴン)、大谷映美里(=LOVE)、池田裕子",
18//     "title": "アッパレやってまーす!~土曜日です~",
19//     "info": "",
20//     "description": "メールアドレス:\u003ca href=mailto:yarudo@mbs1179.com target=_blank\u003eyarudo@mbs1179.com\u003c/a\u003e\u003cbr /\u003e\u003cbr /\u003e\u003cbr /\u003e◆アッパレやってまーす!~土曜日です~番組サイト◆\u003cbr /\u003e☆番組ホームページ:\u003ca href='https://www.mbs1179.com/yaru/' target=_blank\u003eこちらをクリック\u003c/a\u003e\u003cbr /\u003e☆X(旧Twitter):\u003ca href='https://twitter.com/mbs_yarudo/' target=_blank\u003e@mbs_yarudo\u003c/a\u003e",
21//     "status": "past",
22//     "img": "https://program-static.cf.radiko.jp/6ff0b838-2453-4734-ad79-0be88a84b425.jpeg",
23//     "genre": {
24//       "personality": {
25//         "id": "C010",
26//         "name": "タレント"
27//       },
28//       "program": {
29//         "id": "P006",
30//         "name": "バラエティ"
31//       }
32//     },
33//     "ts_in_ng": 0,
34//     "ts_out_ng": 0,
35//     "tsplus_in_ng": 0,
36//     "tsplus_out_ng": 0,
37//     "metas": [
38//       {
39//         "name": "twitter",
40//         "value": "#radiko"
41//       }
42//     ]
43//   },
44//   {
45//     "start_time": "2025-07-06 00:00:00",
46//     "end_time": "2025-07-06 01:30:00",
47//     "start_time_s": "2400",
48//     "end_time_s": "2530",
49//     "program_date": "20250705",
50//     "program_url": "https://www.mbs1179.com/yaru/",
51//     "station_id": "MBS",
52//     "performer": "極楽とんぼ、河合郁人、さらば青春の光(週替わり)、トム・ブラウン(週替わり)、小沢一敬(週替わり)(スピードワゴン)、大谷映美里(=LOVE)、池田裕子",
53//     "title": "アッパレやってまーす!~土曜日です~",
54//     "info": "",
55//     "description": "メールアドレス:\u003ca href=mailto:yarudo@mbs1179.com target=_blank\u003eyarudo@mbs1179.com\u003c/a\u003e\u003cbr /\u003e\u003cbr /\u003e\u003cbr /\u003e◆アッパレやってまーす!~土曜日です~番組サイト◆\u003cbr /\u003e☆番組ホームページ:\u003ca href='https://www.mbs1179.com/yaru/' target=_blank\u003eこちらをクリック\u003c/a\u003e\u003cbr /\u003e☆X(旧Twitter):\u003ca href='https://twitter.com/mbs_yarudo/' target=_blank\u003e@mbs_yarudo\u003c/a\u003e",
56//     "status": "future",
57//     "img": "https://program-static.cf.radiko.jp/6ff0b838-2453-4734-ad79-0be88a84b425.jpeg",
58//     "genre": {
59//       "personality": {
60//         "id": "C010",
61//         "name": "タレント"
62//       },
63//       "program": {
64//         "id": "P006",
65//         "name": "バラエティ"
66//       }
67//     },
68//     "ts_in_ng": 0,
69//     "ts_out_ng": 0,
70//     "tsplus_in_ng": 0,
71//     "tsplus_out_ng": 0,
72//     "metas": [
73//       {
74//         "name": "twitter",
75//         "value": "#radiko"
76//       }
77//     ]
78//   },
79// ]
80// ```
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct Program {
84    #[serde(with = "jst_datetime")]
85    pub start_time: DateTime<Tz>,
86    #[serde(with = "jst_datetime")]
87    pub end_time: DateTime<Tz>,
88    pub start_time_s: String,
89    pub end_time_s: String,
90    pub station_id: String,
91    pub performer: String,
92    pub title: String,
93    pub info: String,
94    pub description: String,
95    pub img: String,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct Programs {
100    pub data: Vec<Program>,
101}
102
103impl Program {
104    pub fn now_to_start_duration(&self, now: Option<DateTime<Tz>>) -> Option<u64> {
105        let now = match now {
106            Some(now) => now,
107            None => Utc::now().with_timezone(&Tokyo),
108        };
109        let duration = self.start_time.signed_duration_since(now).num_seconds();
110        if duration <= 0 {
111            return None;
112        }
113        Some(duration as u64)
114    }
115
116    pub fn start_to_end_duration(&self) -> u64 {
117        self.end_time
118            .signed_duration_since(self.start_time)
119            .num_seconds() as u64
120    }
121
122    pub fn now_to_end_duration(&self, now: Option<DateTime<Tz>>) -> Option<u64> {
123        let now = match now {
124            Some(now) => now,
125            None => Utc::now().with_timezone(&Tokyo),
126        };
127        let duration = self.end_time.signed_duration_since(now).num_seconds();
128        if duration <= 0 {
129            return None;
130        }
131        Some(duration as u64)
132    }
133}
134
135impl From<ProgramXml> for Program {
136    fn from(value: ProgramXml) -> Self {
137        let ft = Tokyo
138            .from_local_datetime(
139                &NaiveDateTime::parse_from_str(&value.ft, "%Y%m%d%H%M%S")
140                    .expect("time parse error"),
141            )
142            .unwrap();
143        let to = Tokyo
144            .from_local_datetime(
145                &NaiveDateTime::parse_from_str(&value.to, "%Y%m%d%H%M%S")
146                    .expect("time parse error"),
147            )
148            .unwrap();
149        Program {
150            start_time: ft,
151            end_time: to,
152            start_time_s: value.ftl.clone(),
153            end_time_s: value.tol.clone(),
154            station_id: "".to_string(),
155            performer: value.pfm.unwrap_or_default(),
156            title: value.title.clone(),
157            info: value.info.unwrap_or_default(),
158            description: value.desc.unwrap_or_default(),
159            img: value.img.unwrap_or_default(),
160        }
161    }
162}
163
164impl From<RadikoProgramXml> for Programs {
165    fn from(value: RadikoProgramXml) -> Self {
166        let mut programs = Vec::new();
167        for station in value.stations.station {
168            for programs_xml in station.programs {
169                for program_xml in programs_xml.program {
170                    let mut program = Program::from(program_xml);
171                    program.station_id = station.id.clone();
172                    programs.push(program);
173                }
174            }
175        }
176        Programs { data: programs }
177    }
178}
179
180/// https://serde.rs/custom-date-format.html
181mod jst_datetime {
182    use chrono::{DateTime, NaiveDateTime, TimeZone};
183    use chrono_tz::{Asia::Tokyo, Tz};
184    use serde::{self, Deserialize, Deserializer, Serializer};
185
186    const FORMAT: &str = "%Y-%m-%d %H:%M:%S";
187
188    // The signature of a serialize_with function must follow the pattern:
189    //
190    //    fn serialize<S>(&T, S) -> Result<S::Ok, S::Error>
191    //    where
192    //        S: Serializer
193    //
194    // although it may also be generic over the input types T.
195    pub fn serialize<S>(date: &DateTime<Tz>, serializer: S) -> Result<S::Ok, S::Error>
196    where
197        S: Serializer,
198    {
199        let s = format!("{}", date.format(FORMAT));
200        serializer.serialize_str(&s)
201    }
202
203    // The signature of a deserialize_with function must follow the pattern:
204    //
205    //    fn deserialize<'de, D>(D) -> Result<T, D::Error>
206    //    where
207    //        D: Deserializer<'de>
208    //
209    // although it may also be generic over the output types T.
210    pub fn deserialize<'de, D>(deserializer: D) -> Result<DateTime<Tz>, D::Error>
211    where
212        D: Deserializer<'de>,
213    {
214        let s = String::deserialize(deserializer)?;
215        let dt = NaiveDateTime::parse_from_str(&s, FORMAT).unwrap();
216        Ok(Tokyo.from_local_datetime(&dt).unwrap())
217    }
218}