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: 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 is_password: bool,
144    pub viewer_count: u64,
145    pub title: String,
146    pub subscription: StationSubscription,
147    // 애청자
148    pub regular: u64,
149}
150
151#[derive(Debug, Clone, Deserialize, Serialize)]
152#[serde(rename_all = "camelCase")]
153pub struct StationSubscription {
154    pub total: u64,
155    pub tier1: u64,
156    pub tier2: u64,
157}
158
159#[derive(Debug, Clone, Deserialize, Serialize)]
160#[serde(rename_all = "camelCase")]
161pub struct StationState {
162    #[serde(rename = "broad_start")]
163    pub broad_start: String,
164    #[serde(rename = "upd")]
165    pub upd: StationUpd,
166}
167
168#[derive(Debug, Clone, Deserialize, Serialize)]
169#[serde(rename_all = "camelCase")]
170pub struct StationUpd {
171    #[serde(rename = "fan_cnt", deserialize_with = "as_u64")]
172    pub fan_count: u64,
173}
174
175#[derive(Debug, Clone, Deserialize, Serialize)]
176#[serde(rename_all = "camelCase")]
177pub struct BroadState {
178    #[serde(rename = "is_password", deserialize_with = "as_bool")]
179    pub is_password: bool,
180    #[serde(rename = "current_sum_viewer", deserialize_with = "as_u64")]
181    pub viewer_count: u64,
182    #[serde(rename = "broad_title")]
183    pub title: String,
184}
185
186#[derive(Debug, Clone, Deserialize, Serialize)]
187#[serde(rename_all = "camelCase")]
188pub struct SubscriptionState {
189    #[serde(rename = "total", deserialize_with = "as_u64")]
190    pub total: u64,
191    #[serde(rename = "tier1", deserialize_with = "as_u64")]
192    pub tier1: u64,
193    #[serde(rename = "tier2", deserialize_with = "as_u64")]
194    pub tier2: u64,
195}
196
197#[derive(Debug, Clone, Deserialize, Serialize)]
198pub struct SignatureEmoticonResponse {
199    #[serde(rename = "result")]
200    pub result: i32,
201    #[serde(rename = "data")]
202    pub data: SignatureEmoticonData,
203}
204
205#[derive(Debug, Clone, Deserialize, Serialize)]
206#[serde(rename_all = "camelCase")]
207pub struct SignatureEmoticonData {
208    #[serde(rename = "tier1")]
209    pub tier_1: Vec<Emoticon>,
210    #[serde(rename = "tier2")]
211    pub tier_2: Vec<Emoticon>,
212}
213
214#[derive(Debug, Clone, Deserialize, Serialize)]
215pub struct Emoticon {
216    #[serde(rename = "title")]
217    pub title: String,
218    #[serde(rename(serialize = "pcImg", deserialize = "pc_img"))]
219    pub pc_img: String,
220    #[serde(rename(serialize = "mobileImg", deserialize = "mobile_img"))]
221    pub mobile_img: String,
222}
223
224#[derive(Debug, Clone, Deserialize, Serialize)]
225#[serde(rename_all = "camelCase")]
226pub struct VOD {
227    pub id: u64,
228    pub title: String,
229    pub thumbnail_url: String,
230    pub duration: u64,
231    pub reg_date: DateTime<Utc>,
232}
233
234#[derive(Debug, Clone, Deserialize, Serialize)]
235#[serde(rename_all = "camelCase")]
236pub struct VODDetail {
237    pub id: String,
238    pub title: String,
239    pub channel_id: String,
240    pub broad_start: String,
241    pub files: Vec<VODFile>,
242}
243
244#[derive(Debug, Clone, Deserialize, Serialize)]
245#[serde(rename_all = "camelCase")]
246pub struct VODFile {
247    pub id: u64,
248    pub order: u32,
249    pub file_key: String,
250    pub file_start: String,
251    pub chat: String,
252    pub duration: u64,
253}
254
255#[derive(Debug, Clone, Serialize)]
256#[serde(rename_all = "camelCase")]
257pub struct VodChatCollection {
258    pub events: Vec<Event>,
259    pub chunks: Vec<VodChatChunk>,
260    pub failures: Vec<VodChatChunkFailure>,
261}
262
263#[derive(Debug, Clone, Deserialize, Serialize)]
264#[serde(rename_all = "camelCase")]
265pub struct VodChatChunk {
266    pub file_id: u64,
267    pub file_order: u32,
268    pub start_time: u64,
269    pub event_count: usize,
270}
271
272#[derive(Debug, Clone, Deserialize, Serialize)]
273#[serde(rename_all = "camelCase")]
274pub struct VodChatChunkFailure {
275    pub file_id: u64,
276    pub file_order: u32,
277    pub start_time: u64,
278    pub reason: String,
279}
280
281#[derive(Debug, Clone, Deserialize, Serialize)]
282pub struct RawVODResponse {
283    data: Vec<RawVOD>,
284}
285
286#[derive(Debug, Clone, Deserialize, Serialize)]
287pub struct RawVOD {
288    title_no: u64,
289    title_name: String,
290    auth_no: u32,
291    reg_date: String,
292    ucc: RawVODUcc,
293}
294
295#[derive(Debug, Clone, Deserialize, Serialize)]
296pub struct RawVODUcc {
297    thumb: Option<String>,
298    total_file_duration: u64,
299}
300
301#[derive(Debug, Clone, Deserialize, Serialize)]
302pub struct RawVODDetailResponse {
303    result: i32,
304    data: Option<RawVODDetailData>,
305}
306
307#[derive(Debug, Clone, Deserialize, Serialize)]
308pub struct RawVODDetailData {
309    title_no: u64,
310    full_title: String,
311    bj_id: String,
312    broad_start: String,
313    files: Vec<RawVODDetailFile>,
314}
315
316#[derive(Debug, Clone, Deserialize, Serialize)]
317pub struct RawVODDetailFile {
318    idx: u64,
319    file_order: u32,
320    file_info_key: String,
321    file_start: String,
322    chat: String,
323    duration: u64,
324}
325
326pub fn parse_soop_timestamp(timestamp: &str) -> Result<DateTime<Utc>> {
327    let mut date = chrono::NaiveDateTime::parse_from_str(timestamp, "%Y-%m-%d %H:%M:%S")
328        .map_err(|e| crate::error::Error::Decode(format!("timestamp parse failed: {e}")))?
329        .and_utc();
330
331    date -= chrono::Duration::hours(9);
332    Ok(date)
333}