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}