sponsor_block/client/user/
segments.rs

1// Uses
2use 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// Function-Specific Deserialization Structs
28#[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	/// Converts a raw segment that more closely matches the structure returned
61	/// by the API to the proper rusty [`Segment`] type.
62	///
63	/// `additional_info` determines whether or not to include
64	/// `RawSegment.additional_info`, since it is always populated by Serde but
65	/// not with useful values under certain circumstances.
66	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
120// Function Implementation
121impl Client {
122	/// Fetches the segments for a given video ID.
123	///
124	/// This function *does not* return additional segment info.
125	///
126	/// # Errors
127	/// Can return pretty much any error type from [`SponsorBlockError`]. See
128	/// the error type definitions for explanations of when they might be
129	/// encountered.
130	///
131	/// The only error types among them you may want to handle differently are
132	/// [`HttpClient(404)`] and [`NoMatchingVideoHash`], as they indicate that
133	/// no videos could be found in the database matching what was provided.
134	///
135	/// [`SponsorBlockError`]: crate::SponsorBlockError
136	/// [`HttpClient(404)`]: crate::SponsorBlockError::HttpClient
137	/// [`NoMatchingVideoHash`]: crate::SponsorBlockError::NoMatchingVideoHash
138	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	/// Fetches the segments for a given video ID.
148	///
149	/// This variant allows you to specify segment UUIDs to require to be
150	/// retrieved, even if they don't meet the minimum vote threshold. If this
151	/// isn't something you need, use the regular [`fetch_segments`] instead.
152	///
153	/// This function *does not* return additional segment info.
154	///
155	/// # Errors
156	/// See the Errors section of the [base version of this
157	/// function](Self::fetch_segments).
158	///
159	/// [`fetch_segments`]: Self::fetch_segments
160	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		// Function Constants
167		const API_ENDPOINT: &str = "/skipSegments";
168
169		// Build the request and send it
170		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		// Deserialize the response and parse it into the output
205		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	/// Fetches complete info for a segment.
233	///
234	/// This function *does* return additional segment info.
235	///
236	/// # Errors
237	/// Can return pretty much any error type from [`SponsorBlockError`]. See
238	/// the error type definitions for explanations of when they might be
239	/// encountered.
240	///
241	/// [`SponsorBlockError`]: crate::SponsorBlockError
242	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	/// Fetches complete info for segments.
254	///
255	/// This function *does* return additional segment info.
256	///
257	/// # Errors
258	/// Can return pretty much any error type from [`SponsorBlockError`]. See
259	/// the error type definitions for explanations of when they might be
260	/// encountered.
261	///
262	/// [`SponsorBlockError`]: crate::SponsorBlockError
263	pub async fn fetch_segment_info_multiple<S: AsRef<SegmentUuidSlice>>(
264		&self,
265		segment_uuids: &[S],
266	) -> Result<Vec<Segment>> {
267		// Function Constants
268		const API_ENDPOINT: &str = "/segmentInfo";
269
270		// Build the request and send it
271		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		// Deserialize the response and parse it into the output
278		from_json_str::<Vec<RawSegment>>(response.as_str())?
279			.drain(..)
280			.map(|s| s.convert_to_segment(true))
281			.collect()
282	}
283}