use crate::api;
use crate::chat::events::Event;
use crate::constants::{EMOTICON_API_URL, PLAYER_LIVE_API_URL};
use crate::error::{Error, Result};
use crate::models::{
LiveDetail, LiveDetailToCheck, LiveStatus, RawLiveDetail, RawStation, RawVODDetailResponse,
RawVODResponse, SignatureEmoticonData, SignatureEmoticonResponse, Station, VOD, VODDetail,
VODFile, VodChatChunk, VodChatChunkFailure, VodChatCollection, parse_soop_timestamp,
};
use crate::vod_chat_parser::parse_vod_chat_xml_with_start_time;
use reqwest::{Client, Response};
#[derive(Debug)]
pub struct SoopHttpClient {
client: Client,
}
impl SoopHttpClient {
pub fn new() -> Self {
Self {
client: api::default_client(),
}
}
pub async fn get_live_status(&self, streamer_id: &str) -> Result<LiveStatus> {
let resp = self.fetch_live_detail_response(streamer_id).await?;
let bytes = resp.bytes().await.map_err(Error::Transport)?;
let live_detail_to_check = serde_json::from_slice::<LiveDetailToCheck>(&bytes)
.map_err(|e| Error::Decode(e.to_string()))?;
if !live_detail_to_check.is_streaming() {
return Ok(LiveStatus::Offline);
}
let live_detail = serde_json::from_slice::<RawLiveDetail>(&bytes)
.map_err(|e| Error::Decode(e.to_string()))?;
Ok(LiveStatus::Live(LiveDetail {
is_live: live_detail_to_check.is_streaming(),
ch_domain: live_detail.channel.ch_domain,
ch_pt: live_detail.channel.ch_pt,
ch_no: live_detail.channel.chat_no,
bno: live_detail.channel.bno,
streamer_nick: live_detail.channel.bj_nick,
title: live_detail.channel.title,
categories: live_detail.channel.categories,
}))
}
pub async fn get_station(&self, streamer_id: &str) -> Result<Station> {
let request = api::with_user_agent(self.client.get(format!(
"https://chapi.sooplive.co.kr/api/{}/station",
streamer_id
)));
let raw = api::send_json::<RawStation>(request).await?;
Ok(Station {
broad_start: parse_soop_timestamp(&raw.station.broad_start)?,
is_password: raw.broad.is_password,
viewer_count: raw.broad.viewer_count,
title: raw.broad.title,
})
}
pub async fn get_signature_emoticon(&self, streamer_id: &str) -> Result<SignatureEmoticonData> {
let params = [("szBjId", streamer_id), ("work", "list"), ("v", "tier")];
let request = api::with_form_headers(self.client.post(EMOTICON_API_URL)).form(¶ms);
let emoticon_response = api::send_json::<SignatureEmoticonResponse>(request).await?;
Ok(emoticon_response.data)
}
async fn fetch_live_detail_response(&self, streamer_id: &str) -> Result<Response> {
let params = [("bid", streamer_id)];
let request = api::with_form_headers(self.client.post(PLAYER_LIVE_API_URL)).form(¶ms);
api::send(request).await
}
pub async fn get_vod_list(&self, streamer_id: &str, page: u32) -> Result<Vec<VOD>> {
let url = format!(
"https://chapi.sooplive.co.kr/api/{}/vods/review?page={}&per_page=60&orderby=reg_date&field=title%2Ccontents&created=false",
streamer_id, page
);
let request = api::with_user_agent(self.client.get(&url));
let vod_response = api::send_json::<RawVODResponse>(request).await?;
vod_response.into_vods()
}
pub async fn get_vod_detail(&self, vod_id: u64) -> Result<VODDetail> {
let params = [("nTitleNo", vod_id.to_string())];
let request = api::with_form_headers(
self.client
.post("https://api.m.sooplive.co.kr/station/video/a/view"),
)
.form(¶ms);
let vod_detail_response = api::send_json::<RawVODDetailResponse>(request).await?;
vod_detail_response.into_vod_detail()
}
pub async fn get_vod_chat(&self, chat_url: &str, start_time: u64) -> Result<String> {
let url = format!("{}&startTime={}", chat_url, start_time);
let request = api::with_user_agent(self.client.get(&url));
api::send_text(request).await
}
async fn get_file_chat_events(
&self,
file: &VODFile,
broad_start: &str,
chunk_size_seconds: u64,
) -> (Vec<Event>, Vec<VodChatChunk>, Vec<VodChatChunkFailure>) {
let duration_seconds = file.duration / 1_000_000; let mut all_events = Vec::new();
let mut chunks = Vec::new();
let mut failures = Vec::new();
let mut current_time = 0;
while current_time < duration_seconds {
match self.get_vod_chat(&file.chat, current_time).await {
Ok(xml_content) => {
if !xml_content.trim().is_empty() {
match parse_vod_chat_xml_with_start_time(&xml_content, Some(broad_start)) {
Ok(mut events) => {
let event_count = events.len();
all_events.append(&mut events);
chunks.push(VodChatChunk {
file_id: file.id,
file_order: file.order,
start_time: current_time,
event_count,
});
}
Err(e) => {
failures.push(VodChatChunkFailure {
file_id: file.id,
file_order: file.order,
start_time: current_time,
reason: e.to_string(),
});
}
}
}
}
Err(e) => {
failures.push(VodChatChunkFailure {
file_id: file.id,
file_order: file.order,
start_time: current_time,
reason: e.to_string(),
});
}
}
current_time += chunk_size_seconds;
}
(all_events, chunks, failures)
}
pub async fn get_full_vod_chat(&self, vod_id: u64) -> Result<VodChatCollection> {
let vod_detail = self.get_vod_detail(vod_id).await?;
let mut all_events = Vec::new();
let mut chunks = Vec::new();
let mut failures = Vec::new();
for file in vod_detail.files.iter() {
let (mut file_events, mut file_chunks, mut file_failures) = self
.get_file_chat_events(file, &vod_detail.broad_start, 300)
.await; all_events.append(&mut file_events);
chunks.append(&mut file_chunks);
failures.append(&mut file_failures);
}
Ok(VodChatCollection {
events: all_events,
chunks,
failures,
})
}
}
impl Default for SoopHttpClient {
fn default() -> Self {
Self::new()
}
}