sponsor_block/client/user/
segments.rs1use serde::Deserialize;
3use serde_json::from_str as from_json_str;
4#[cfg(feature = "private_searches")]
5use sha2::{Digest, Sha256};
6
7#[cfg(feature = "private_searches")]
8use crate::util::bytes_to_hex_string;
9use crate::{
10 api::convert_category_bitflags_to_url,
11 error::{Result, SponsorBlockError},
12 segment::{AcceptedCategories, ActionableSegmentKind, Segment},
13 util::{
14 de::{bool_from_integer_str, none_on_0_0_from_str},
15 get_response_text,
16 to_url_array,
17 },
18 Action,
19 AdditionalSegmentInfo,
20 Client,
21 SegmentUuid,
22 SegmentUuidSlice,
23 VideoId,
24 VideoIdSlice,
25};
26
27#[cfg(feature = "private_searches")]
29#[derive(Deserialize, Debug, Default)]
30#[serde(default)]
31struct RawHashMatch {
32 #[serde(rename = "videoID")]
33 video_id: VideoId,
34 hash: String,
35 segments: Vec<RawSegment>,
36}
37
38#[derive(Deserialize, Debug, Default)]
39#[serde(default, rename_all = "camelCase")]
40struct RawSegment {
41 category: ActionableSegmentKind,
42 #[serde(rename = "actionType")]
43 action_type: Action,
44 #[serde(rename = "segment")]
45 time_points: Option<[f32; 2]>,
46 start_time: Option<f32>,
47 end_time: Option<f32>,
48 #[serde(rename = "UUID")]
49 uuid: SegmentUuid,
50 #[serde(deserialize_with = "bool_from_integer_str")]
51 locked: bool,
52 votes: i32,
53 #[serde(rename = "videoDuration", deserialize_with = "none_on_0_0_from_str")]
54 video_duration_upon_submission: Option<f32>,
55 #[serde(flatten)]
56 additional_info: AdditionalSegmentInfo,
57}
58
59impl RawSegment {
60 fn convert_to_segment(self, additional_info: bool) -> Result<Segment> {
67 let time_points = if let Some(points) = self.time_points {
68 points
69 } else {
70 [
71 self.start_time
72 .expect("time_points was empty but so is start_time"),
73 self.end_time
74 .expect("time_points was empty but so is end_time"),
75 ]
76 };
77 if time_points[0] > time_points[1] {
78 return Err(SponsorBlockError::BadData(format!(
79 "segment start ({}) > end ({})",
80 time_points[0], time_points[1]
81 )));
82 }
83 if time_points[0] < 0.0 {
84 return Err(SponsorBlockError::BadData(format!(
85 "segment start ({}) < 0",
86 time_points[0]
87 )));
88 }
89 if time_points[1] < 0.0 {
90 return Err(SponsorBlockError::BadData(format!(
91 "segment end ({}) < 0",
92 time_points[1]
93 )));
94 }
95 if let Some(video_duration_upon_submission) = self.video_duration_upon_submission {
96 if video_duration_upon_submission < 0.0 {
97 return Err(SponsorBlockError::BadData(format!(
98 "video duration upon submission ({}) < 0",
99 video_duration_upon_submission
100 )));
101 }
102 }
103
104 Ok(Segment {
105 segment: self.category.to_actionable_segment(time_points),
106 action_type: self.action_type,
107 uuid: self.uuid,
108 locked: self.locked,
109 votes: self.votes,
110 video_duration_on_submission: self.video_duration_upon_submission,
111 additional_info: if additional_info {
112 Some(self.additional_info)
113 } else {
114 None
115 },
116 })
117 }
118}
119
120impl Client {
122 pub async fn fetch_segments(
139 &self,
140 video_id: &VideoIdSlice,
141 accepted_categories: AcceptedCategories,
142 ) -> Result<Vec<Segment>> {
143 self.fetch_segments_with_required::<&SegmentUuidSlice>(video_id, accepted_categories, &[])
144 .await
145 }
146
147 pub async fn fetch_segments_with_required<S: AsRef<SegmentUuidSlice>>(
161 &self,
162 video_id: &VideoIdSlice,
163 accepted_categories: AcceptedCategories,
164 required_segments: &[S],
165 ) -> Result<Vec<Segment>> {
166 const API_ENDPOINT: &str = "/skipSegments";
168
169 let mut request;
171 #[cfg(not(feature = "private_searches"))]
172 {
173 request = self
174 .http
175 .get(format!("{}{}", &self.base_url, API_ENDPOINT))
176 .query(&[("videoID", video_id)]);
177 }
178 #[cfg(feature = "private_searches")]
179 {
180 let video_id_hash = {
181 let mut hasher = Sha256::new();
182 Digest::update(&mut hasher, video_id.as_bytes());
183 bytes_to_hex_string(&hasher.finalize()[..])
184 };
185 request = self.http.get(format!(
186 "{}{}/{}",
187 &self.base_url,
188 API_ENDPOINT,
189 &video_id_hash[0..self.hash_prefix_length as usize]
190 ));
191 }
192
193 request = request
194 .query(&[(
195 "categories",
196 convert_category_bitflags_to_url(accepted_categories),
197 )])
198 .query(&[("service", &self.service)]);
199 if !required_segments.is_empty() {
200 request = request.query(&[("requiredSegments", to_url_array(required_segments))]);
201 }
202 let response = get_response_text(request.send().await?).await?;
203
204 let mut video_segments;
206 #[cfg(not(feature = "private_searches"))]
207 {
208 video_segments = from_json_str::<Vec<RawSegment>>(response.as_str())?;
209 }
210 #[cfg(feature = "private_searches")]
211 {
212 let mut found_match = false;
213 video_segments = Vec::new();
214 for hash_match in from_json_str::<Vec<RawHashMatch>>(response.as_str())?.drain(..) {
215 if hash_match.video_id == video_id {
216 video_segments = hash_match.segments;
217 found_match = true;
218 break;
219 }
220 }
221 if !found_match {
222 return Err(SponsorBlockError::NoMatchingVideoHash);
223 }
224 }
225
226 video_segments
227 .drain(..)
228 .map(|s| s.convert_to_segment(false))
229 .collect()
230 }
231
232 pub async fn fetch_segment_info<S: AsRef<SegmentUuidSlice>>(
243 &self,
244 segment_uuid: S,
245 ) -> Result<Segment> {
246 Ok(self
247 .fetch_segment_info_multiple(&[segment_uuid])
248 .await?
249 .pop()
250 .ok_or_else(|| SponsorBlockError::BadData("no segments found".to_owned()))?)
251 }
252
253 pub async fn fetch_segment_info_multiple<S: AsRef<SegmentUuidSlice>>(
264 &self,
265 segment_uuids: &[S],
266 ) -> Result<Vec<Segment>> {
267 const API_ENDPOINT: &str = "/segmentInfo";
269
270 let request = self
272 .http
273 .get(format!("{}{}", &self.base_url, API_ENDPOINT))
274 .query(&[("UUIDs", to_url_array(segment_uuids))]);
275 let response = get_response_text(request.send().await?).await?;
276
277 from_json_str::<Vec<RawSegment>>(response.as_str())?
279 .drain(..)
280 .map(|s| s.convert_to_segment(true))
281 .collect()
282 }
283}