Skip to main content

soup_sdk/
models.rs

1use crate::error::Result;
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4use serde_this_or_that::{as_bool, as_u64};
5
6use crate::chat::events::Event;
7// --- LiveDetail 관련 구조체들 ---
8
9#[derive(Debug, Clone, Deserialize, Serialize)]
10pub struct RawLiveDetail {
11    #[serde(rename = "CHANNEL")]
12    pub channel: ChannelInfo,
13}
14
15#[derive(Debug, Clone, Deserialize, Serialize)]
16#[serde(rename_all = "camelCase")]
17pub struct LiveDetail {
18    pub is_live: bool,
19    pub ch_domain: String,
20    pub ch_pt: u64,
21    pub ch_no: String,
22    pub bno: u64,
23    pub streamer_nick: String,
24    pub title: String,
25    pub categories: Vec<String>,
26}
27
28#[derive(Debug, Clone, Deserialize, Serialize)]
29#[serde(rename_all = "camelCase")]
30pub enum LiveStatus {
31    Offline,
32    Live(LiveDetail),
33}
34
35#[derive(Debug, Clone, Deserialize, Serialize)]
36pub struct LiveDetailToCheck {
37    #[serde(rename = "CHANNEL")]
38    pub channel: ChannelInfoToCheck,
39}
40
41#[derive(Debug, Clone, Deserialize, Serialize)]
42pub struct ChannelInfoToCheck {
43    #[serde(rename = "RESULT")]
44    pub result: i32, // 1이면 방송 중, 0이면 방송 중 아님
45}
46
47#[derive(Debug, Clone, Deserialize, Serialize)]
48#[serde(rename_all = "camelCase")]
49pub struct ChannelInfo {
50    #[serde(rename = "RESULT")]
51    pub result: i32, // 1이면 방송 중, 0이면 방송 중 아님
52    #[serde(rename = "BNO", deserialize_with = "as_u64")]
53    pub bno: u64,
54    #[serde(rename = "CHDOMAIN")]
55    pub ch_domain: String,
56    #[serde(rename = "CHPT", deserialize_with = "as_u64")]
57    pub ch_pt: u64,
58    #[serde(rename = "CHATNO")]
59    pub chat_no: String,
60    #[serde(rename = "BJNICK")]
61    pub bj_nick: String,
62    #[serde(rename = "TITLE")]
63    pub title: String,
64    #[serde(rename = "CATEGORY_TAGS")]
65    pub categories: Vec<String>,
66}
67
68impl RawVODResponse {
69    pub fn into_vods(self) -> Result<Vec<VOD>> {
70        self.data
71            .into_iter()
72            .filter(|vod| vod.auth_no == 101)
73            .map(|vod| {
74                Ok(VOD {
75                    id: vod.title_no,
76                    title: vod.title_name,
77                    thumbnail_url: vod
78                        .ucc
79                        .thumb
80                        .as_ref()
81                        .map(|s| format!("https:{}", s))
82                        .unwrap_or_default(),
83                    duration: vod.ucc.total_file_duration,
84                    reg_date: parse_soop_timestamp(&vod.reg_date)?,
85                })
86            })
87            .collect()
88    }
89}
90
91impl RawVODDetailResponse {
92    pub fn into_vod_detail(self) -> Result<VODDetail> {
93        if self.result != 1 {
94            return Err(crate::error::Error::Protocol("VOD not found".to_string()));
95        }
96
97        let data = self
98            .data
99            .ok_or_else(|| crate::error::Error::Protocol("VOD data not available".to_string()))?;
100
101        Ok(VODDetail {
102            id: data.title_no.to_string(),
103            title: data.full_title,
104            channel_id: data.bj_id,
105            broad_start: data.broad_start,
106            files: data
107                .files
108                .into_iter()
109                .map(|file| VODFile {
110                    id: file.idx,
111                    order: file.file_order,
112                    file_key: file.file_info_key,
113                    file_start: file.file_start,
114                    chat: file.chat,
115                    duration: file.duration,
116                })
117                .collect(),
118        })
119    }
120}
121
122impl LiveDetailToCheck {
123    // 방송 중인지 여부를 쉽게 확인할 수 있는 헬퍼 메서드
124    pub fn is_streaming(&self) -> bool {
125        self.channel.result == 1
126    }
127}
128
129#[derive(Debug, Clone, Deserialize, Serialize)]
130pub struct RawStation {
131    #[serde(rename = "station")]
132    pub station: StationState,
133    #[serde(rename = "broad")]
134    pub broad: Option<BroadState>,
135    #[serde(rename = "subscription")]
136    pub subscription: SubscriptionState,
137}
138
139#[derive(Debug, Clone, Deserialize, Serialize)]
140#[serde(rename_all = "camelCase")]
141pub struct Station {
142    pub broad_start: DateTime<Utc>,
143    pub broad: Option<BroadState>,
144    pub subscription: StationSubscription,
145    // 애청자
146    pub regular: u64,
147}
148
149#[derive(Debug, Clone, Deserialize, Serialize)]
150#[serde(rename_all = "camelCase")]
151pub struct StationSubscription {
152    pub total: u64,
153    pub tier1: u64,
154    pub tier2: u64,
155}
156
157#[derive(Debug, Clone, Deserialize, Serialize)]
158#[serde(rename_all = "camelCase")]
159pub struct StationState {
160    #[serde(rename = "broad_start")]
161    pub broad_start: String,
162    #[serde(rename = "upd")]
163    pub upd: StationUpd,
164}
165
166#[derive(Debug, Clone, Deserialize, Serialize)]
167#[serde(rename_all = "camelCase")]
168pub struct StationUpd {
169    #[serde(rename = "fan_cnt", deserialize_with = "as_u64")]
170    pub fan_count: u64,
171}
172
173#[derive(Debug, Clone, Deserialize, Serialize)]
174#[serde(rename_all = "camelCase")]
175pub struct BroadState {
176    #[serde(rename = "is_password", deserialize_with = "as_bool")]
177    pub is_password: bool,
178    #[serde(rename = "current_sum_viewer", deserialize_with = "as_u64")]
179    pub viewer_count: u64,
180    #[serde(rename = "broad_title")]
181    pub title: String,
182    #[serde(rename = "broad_no", deserialize_with = "as_u64")]
183    pub bno: u64,
184    #[serde(rename = "subscription_only", deserialize_with = "as_bool")]
185    pub subscription_only: bool,
186}
187
188#[derive(Debug, Clone, Deserialize, Serialize)]
189#[serde(rename_all = "camelCase")]
190pub struct SubscriptionState {
191    #[serde(rename = "total", deserialize_with = "as_u64")]
192    pub total: u64,
193    #[serde(rename = "tier1", deserialize_with = "as_u64")]
194    pub tier1: u64,
195    #[serde(rename = "tier2", deserialize_with = "as_u64")]
196    pub tier2: u64,
197}
198
199#[derive(Debug, Clone, Deserialize, Serialize)]
200pub struct SignatureEmoticonResponse {
201    #[serde(rename = "result")]
202    pub result: i32,
203    #[serde(rename = "data")]
204    pub data: SignatureEmoticonData,
205}
206
207#[derive(Debug, Clone, Deserialize, Serialize)]
208#[serde(rename_all = "camelCase")]
209pub struct SignatureEmoticonData {
210    #[serde(rename = "tier1")]
211    pub tier_1: Vec<Emoticon>,
212    #[serde(rename = "tier2")]
213    pub tier_2: Vec<Emoticon>,
214}
215
216#[derive(Debug, Clone, Deserialize, Serialize)]
217pub struct Emoticon {
218    #[serde(rename = "title")]
219    pub title: String,
220    #[serde(rename(serialize = "pcImg", deserialize = "pc_img"))]
221    pub pc_img: String,
222    #[serde(rename(serialize = "mobileImg", deserialize = "mobile_img"))]
223    pub mobile_img: String,
224}
225
226#[derive(Debug, Clone, Deserialize, Serialize)]
227#[serde(rename_all = "camelCase")]
228pub struct VOD {
229    pub id: u64,
230    pub title: String,
231    pub thumbnail_url: String,
232    pub duration: u64,
233    pub reg_date: DateTime<Utc>,
234}
235
236#[derive(Debug, Clone, Deserialize, Serialize)]
237#[serde(rename_all = "camelCase")]
238pub struct VODDetail {
239    pub id: String,
240    pub title: String,
241    pub channel_id: String,
242    pub broad_start: String,
243    pub files: Vec<VODFile>,
244}
245
246#[derive(Debug, Clone, Deserialize, Serialize)]
247#[serde(rename_all = "camelCase")]
248pub struct VODFile {
249    pub id: u64,
250    pub order: u32,
251    pub file_key: String,
252    pub file_start: String,
253    pub chat: String,
254    pub duration: u64,
255}
256
257#[derive(Debug, Clone, Serialize)]
258#[serde(rename_all = "camelCase")]
259pub struct VodChatCollection {
260    pub events: Vec<Event>,
261    pub chunks: Vec<VodChatChunk>,
262    pub failures: Vec<VodChatChunkFailure>,
263}
264
265#[derive(Debug, Clone, Deserialize, Serialize)]
266#[serde(rename_all = "camelCase")]
267pub struct VodChatChunk {
268    pub file_id: u64,
269    pub file_order: u32,
270    pub start_time: u64,
271    pub event_count: usize,
272}
273
274#[derive(Debug, Clone, Deserialize, Serialize)]
275#[serde(rename_all = "camelCase")]
276pub struct VodChatChunkFailure {
277    pub file_id: u64,
278    pub file_order: u32,
279    pub start_time: u64,
280    pub reason: String,
281}
282
283#[derive(Debug, Clone, Deserialize, Serialize)]
284pub struct RawVODResponse {
285    data: Vec<RawVOD>,
286}
287
288#[derive(Debug, Clone, Deserialize, Serialize)]
289pub struct RawVOD {
290    title_no: u64,
291    title_name: String,
292    auth_no: u32,
293    reg_date: String,
294    ucc: RawVODUcc,
295}
296
297#[derive(Debug, Clone, Deserialize, Serialize)]
298pub struct RawVODUcc {
299    thumb: Option<String>,
300    total_file_duration: u64,
301}
302
303#[derive(Debug, Clone, Deserialize, Serialize)]
304pub struct RawVODDetailResponse {
305    result: i32,
306    data: Option<RawVODDetailData>,
307}
308
309#[derive(Debug, Clone, Deserialize, Serialize)]
310pub struct RawVODDetailData {
311    title_no: u64,
312    full_title: String,
313    bj_id: String,
314    broad_start: String,
315    files: Vec<RawVODDetailFile>,
316}
317
318#[derive(Debug, Clone, Deserialize, Serialize)]
319pub struct RawVODDetailFile {
320    idx: u64,
321    file_order: u32,
322    file_info_key: String,
323    file_start: String,
324    chat: String,
325    duration: u64,
326}
327
328pub fn parse_soop_timestamp(timestamp: &str) -> Result<DateTime<Utc>> {
329    let mut date = chrono::NaiveDateTime::parse_from_str(timestamp, "%Y-%m-%d %H:%M:%S")
330        .map_err(|e| crate::error::Error::Decode(format!("timestamp parse failed: {e}")))?
331        .and_utc();
332
333    date -= chrono::Duration::hours(9);
334    Ok(date)
335}