1use crate::ids::{Aid, Bvid, Cid};
2use crate::{BpiError, BpiResult};
3
4#[derive(Debug, Clone, PartialEq, Eq)]
6pub enum VideoId {
7 Aid(Aid),
9 Bvid(Bvid),
11}
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct VideoViewParams {
16 id: VideoId,
17}
18
19impl VideoViewParams {
20 pub fn from_aid(aid: Aid) -> Self {
22 Self {
23 id: VideoId::Aid(aid),
24 }
25 }
26
27 pub fn from_bvid(bvid: Bvid) -> Self {
29 Self {
30 id: VideoId::Bvid(bvid),
31 }
32 }
33
34 pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
35 video_id_query_pairs(&self.id)
36 }
37}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct VideoDetailParams {
42 id: VideoId,
43 need_elec: Option<u8>,
44}
45
46impl VideoDetailParams {
47 pub fn from_aid(aid: Aid) -> Self {
49 Self::new(VideoId::Aid(aid))
50 }
51
52 pub fn from_bvid(bvid: Bvid) -> Self {
54 Self::new(VideoId::Bvid(bvid))
55 }
56
57 fn new(id: VideoId) -> Self {
58 Self {
59 id,
60 need_elec: None,
61 }
62 }
63
64 pub fn need_elec(mut self, need_elec: bool) -> Self {
66 self.need_elec = Some(u8::from(need_elec));
67 self
68 }
69
70 pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
71 let mut params = video_id_query_pairs(&self.id);
72
73 if let Some(need_elec) = self.need_elec {
74 params.push(("need_elec", need_elec.to_string()));
75 }
76
77 params
78 }
79}
80
81#[derive(Debug, Clone, PartialEq, Eq)]
83pub struct VideoPageListParams {
84 id: VideoId,
85}
86
87impl VideoPageListParams {
88 pub fn from_aid(aid: Aid) -> Self {
90 Self {
91 id: VideoId::Aid(aid),
92 }
93 }
94
95 pub fn from_bvid(bvid: Bvid) -> Self {
97 Self {
98 id: VideoId::Bvid(bvid),
99 }
100 }
101
102 pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
103 video_id_query_pairs(&self.id)
104 }
105}
106
107#[derive(Debug, Clone, PartialEq, Eq)]
109pub struct VideoDescParams {
110 id: VideoId,
111}
112
113impl VideoDescParams {
114 pub fn from_aid(aid: Aid) -> Self {
116 Self {
117 id: VideoId::Aid(aid),
118 }
119 }
120
121 pub fn from_bvid(bvid: Bvid) -> Self {
123 Self {
124 id: VideoId::Bvid(bvid),
125 }
126 }
127
128 pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
129 video_id_query_pairs(&self.id)
130 }
131}
132
133#[derive(Debug, Clone, PartialEq, Eq)]
135pub struct VideoPlayUrlParams {
136 id: VideoId,
137 cid: Cid,
138 qn: Option<u64>,
139 fnval: Option<u64>,
140 fnver: Option<u64>,
141 fourk: Option<u8>,
142 platform: String,
143 high_quality: Option<u8>,
144 try_look: Option<u8>,
145}
146
147impl VideoPlayUrlParams {
148 pub fn from_aid(aid: Aid, cid: Cid) -> Self {
150 Self::new(VideoId::Aid(aid), cid)
151 }
152
153 pub fn from_bvid(bvid: Bvid, cid: Cid) -> Self {
155 Self::new(VideoId::Bvid(bvid), cid)
156 }
157
158 fn new(id: VideoId, cid: Cid) -> Self {
159 Self {
160 id,
161 cid,
162 qn: None,
163 fnval: None,
164 fnver: None,
165 fourk: None,
166 platform: "pc".to_string(),
167 high_quality: None,
168 try_look: None,
169 }
170 }
171
172 pub fn quality(mut self, qn: u64) -> Self {
174 self.qn = Some(qn);
175 self
176 }
177
178 pub fn format_flags(mut self, fnval: u64) -> Self {
180 self.fnval = Some(fnval);
181 self
182 }
183
184 pub fn format_version(mut self, fnver: u64) -> Self {
186 self.fnver = Some(fnver);
187 self
188 }
189
190 pub fn fourk(mut self, enabled: bool) -> Self {
192 self.fourk = Some(u8::from(enabled));
193 self
194 }
195
196 pub fn platform(mut self, platform: impl Into<String>) -> Self {
198 self.platform = platform.into();
199 self
200 }
201
202 pub fn high_quality(mut self, enabled: bool) -> Self {
204 self.high_quality = Some(u8::from(enabled));
205 self
206 }
207
208 pub fn try_look(mut self, enabled: bool) -> Self {
210 self.try_look = Some(u8::from(enabled));
211 self
212 }
213
214 pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
215 let mut params = vec![("cid", self.cid.to_string())];
216
217 match &self.id {
218 VideoId::Aid(aid) => params.push(("avid", aid.to_string())),
219 VideoId::Bvid(bvid) => params.push(("bvid", bvid.to_string())),
220 }
221 if let Some(qn) = self.qn {
222 params.push(("qn", qn.to_string()));
223 }
224 if let Some(fnval) = self.fnval {
225 params.push(("fnval", fnval.to_string()));
226 }
227 if let Some(fnver) = self.fnver {
228 params.push(("fnver", fnver.to_string()));
229 }
230 if let Some(fourk) = self.fourk {
231 params.push(("fourk", fourk.to_string()));
232 }
233 params.push(("platform", self.platform.clone()));
234 if let Some(high_quality) = self.high_quality {
235 params.push(("high_quality", high_quality.to_string()));
236 }
237 if let Some(try_look) = self.try_look {
238 params.push(("try_look", try_look.to_string()));
239 }
240
241 params
242 }
243}
244
245#[derive(Debug, Clone, PartialEq, Eq)]
247pub struct VideoOnlineTotalParams {
248 id: VideoId,
249 cid: Cid,
250}
251
252impl VideoOnlineTotalParams {
253 pub fn from_aid(aid: Aid, cid: Cid) -> Self {
254 Self {
255 id: VideoId::Aid(aid),
256 cid,
257 }
258 }
259
260 pub fn from_bvid(bvid: Bvid, cid: Cid) -> Self {
261 Self {
262 id: VideoId::Bvid(bvid),
263 cid,
264 }
265 }
266
267 pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
268 let mut params = vec![("cid", self.cid.to_string())];
269 params.extend(video_id_query_pairs(&self.id));
270 params
271 }
272}
273
274#[derive(Debug, Clone, PartialEq, Eq)]
276pub struct VideoPlayerInfoParams {
277 id: VideoId,
278 cid: Cid,
279 season_id: Option<u64>,
280 ep_id: Option<u64>,
281}
282
283impl VideoPlayerInfoParams {
284 pub fn from_aid(aid: Aid, cid: Cid) -> Self {
285 Self::new(VideoId::Aid(aid), cid)
286 }
287
288 pub fn from_bvid(bvid: Bvid, cid: Cid) -> Self {
289 Self::new(VideoId::Bvid(bvid), cid)
290 }
291
292 fn new(id: VideoId, cid: Cid) -> Self {
293 Self {
294 id,
295 cid,
296 season_id: None,
297 ep_id: None,
298 }
299 }
300
301 pub fn season_id(mut self, season_id: u64) -> BpiResult<Self> {
302 self.season_id = Some(validate_nonzero_u64("season_id", season_id)?);
303 Ok(self)
304 }
305
306 pub fn ep_id(mut self, ep_id: u64) -> BpiResult<Self> {
307 self.ep_id = Some(validate_nonzero_u64("ep_id", ep_id)?);
308 Ok(self)
309 }
310
311 pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
312 let mut params = vec![("cid", self.cid.to_string())];
313 params.extend(video_id_query_pairs(&self.id));
314 if let Some(season_id) = self.season_id {
315 params.push(("season_id", season_id.to_string()));
316 }
317 if let Some(ep_id) = self.ep_id {
318 params.push(("ep_id", ep_id.to_string()));
319 }
320 params
321 }
322}
323
324#[derive(Debug, Clone, PartialEq, Eq)]
326pub struct VideoRelatedParams {
327 id: VideoId,
328}
329
330impl VideoRelatedParams {
331 pub fn from_aid(aid: Aid) -> Self {
332 Self {
333 id: VideoId::Aid(aid),
334 }
335 }
336
337 pub fn from_bvid(bvid: Bvid) -> Self {
338 Self {
339 id: VideoId::Bvid(bvid),
340 }
341 }
342
343 pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
344 video_id_query_pairs(&self.id)
345 }
346}
347
348#[derive(Debug, Clone, Copy, PartialEq, Eq)]
350pub struct VideoHomepageRecommendationsParams {
351 page_size: u8,
352 fresh_idx: u32,
353 fetch_row: u32,
354}
355
356impl VideoHomepageRecommendationsParams {
357 pub fn new() -> Self {
358 Self {
359 page_size: 12,
360 fresh_idx: 1,
361 fetch_row: 1,
362 }
363 }
364
365 pub fn page_size(mut self, page_size: u8) -> BpiResult<Self> {
366 if page_size == 0 || page_size > 30 {
367 return Err(BpiError::invalid_parameter(
368 "ps",
369 "value must be between 1 and 30",
370 ));
371 }
372
373 self.page_size = page_size;
374 Ok(self)
375 }
376
377 pub fn fresh_idx(mut self, fresh_idx: u32) -> BpiResult<Self> {
378 self.fresh_idx = validate_nonzero_u32("fresh_idx", fresh_idx)?;
379 Ok(self)
380 }
381
382 pub fn fetch_row(mut self, fetch_row: u32) -> BpiResult<Self> {
383 self.fetch_row = validate_nonzero_u32("fetch_row", fetch_row)?;
384 Ok(self)
385 }
386
387 pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
388 vec![
389 ("fresh_type", "4".to_string()),
390 ("ps", self.page_size.to_string()),
391 ("fresh_idx", self.fresh_idx.to_string()),
392 ("fresh_idx_1h", self.fresh_idx.to_string()),
393 ("brush", self.fresh_idx.to_string()),
394 ("fetch_row", self.fetch_row.to_string()),
395 ]
396 }
397}
398
399impl Default for VideoHomepageRecommendationsParams {
400 fn default() -> Self {
401 Self::new()
402 }
403}
404
405#[derive(Debug, Clone, PartialEq, Eq)]
407pub struct VideoAiSummaryParams {
408 id: VideoId,
409 cid: Cid,
410 up_mid: u64,
411}
412
413impl VideoAiSummaryParams {
414 pub fn from_aid(aid: Aid, cid: Cid, up_mid: u64) -> BpiResult<Self> {
415 Self::new(VideoId::Aid(aid), cid, up_mid)
416 }
417
418 pub fn from_bvid(bvid: Bvid, cid: Cid, up_mid: u64) -> BpiResult<Self> {
419 Self::new(VideoId::Bvid(bvid), cid, up_mid)
420 }
421
422 fn new(id: VideoId, cid: Cid, up_mid: u64) -> BpiResult<Self> {
423 Ok(Self {
424 id,
425 cid,
426 up_mid: validate_nonzero_u64("up_mid", up_mid)?,
427 })
428 }
429
430 pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
431 let mut params = vec![
432 ("cid", self.cid.to_string()),
433 ("up_mid", self.up_mid.to_string()),
434 ];
435 params.extend(video_id_query_pairs(&self.id));
436 params
437 }
438}
439
440#[derive(Debug, Clone, PartialEq, Eq)]
442pub struct VideoTagsParams {
443 id: VideoId,
444 cid: Option<Cid>,
445}
446
447impl VideoTagsParams {
448 pub fn from_aid(aid: Aid) -> Self {
449 Self {
450 id: VideoId::Aid(aid),
451 cid: None,
452 }
453 }
454
455 pub fn from_bvid(bvid: Bvid) -> Self {
456 Self {
457 id: VideoId::Bvid(bvid),
458 cid: None,
459 }
460 }
461
462 pub fn cid(mut self, cid: Cid) -> Self {
463 self.cid = Some(cid);
464 self
465 }
466
467 pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
468 let mut params = video_id_query_pairs(&self.id);
469 if let Some(cid) = self.cid {
470 params.push(("cid", cid.to_string()));
471 }
472 params
473 }
474}
475
476#[derive(Debug, Clone, PartialEq, Eq)]
478pub struct InteractiveVideoInfoParams {
479 id: VideoId,
480 graph_version: u64,
481 edge_id: Option<u64>,
482}
483
484impl InteractiveVideoInfoParams {
485 pub fn from_aid(aid: Aid, graph_version: u64) -> BpiResult<Self> {
486 Self::new(VideoId::Aid(aid), graph_version)
487 }
488
489 pub fn from_bvid(bvid: Bvid, graph_version: u64) -> BpiResult<Self> {
490 Self::new(VideoId::Bvid(bvid), graph_version)
491 }
492
493 fn new(id: VideoId, graph_version: u64) -> BpiResult<Self> {
494 Ok(Self {
495 id,
496 graph_version: validate_nonzero_u64("graph_version", graph_version)?,
497 edge_id: None,
498 })
499 }
500
501 pub fn edge_id(mut self, edge_id: u64) -> BpiResult<Self> {
502 self.edge_id = Some(validate_nonzero_u64("edge_id", edge_id)?);
503 Ok(self)
504 }
505
506 pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
507 let mut params = vec![("graph_version", self.graph_version.to_string())];
508 params.extend(video_id_query_pairs(&self.id));
509 if let Some(edge_id) = self.edge_id {
510 params.push(("edge_id", edge_id.to_string()));
511 }
512 params
513 }
514}
515
516fn video_id_query_pairs(id: &VideoId) -> Vec<(&'static str, String)> {
517 match id {
518 VideoId::Aid(aid) => vec![("aid", aid.to_string())],
519 VideoId::Bvid(bvid) => vec![("bvid", bvid.to_string())],
520 }
521}
522
523fn validate_nonzero_u32(field: &'static str, value: u32) -> BpiResult<u32> {
524 if value == 0 {
525 return Err(BpiError::invalid_parameter(field, "value must be non-zero"));
526 }
527
528 Ok(value)
529}
530
531fn validate_nonzero_u64(field: &'static str, value: u64) -> BpiResult<u64> {
532 if value == 0 {
533 return Err(BpiError::invalid_parameter(field, "value must be non-zero"));
534 }
535
536 Ok(value)
537}
538
539#[cfg(test)]
540mod tests {
541 use super::*;
542 use crate::BpiError;
543 use crate::ids::{Aid, Cid};
544
545 #[test]
546 fn video_view_params_serializes_bvid_query() -> Result<(), BpiError> {
547 let params = VideoViewParams::from_bvid("BV1xx411c7mD".parse()?);
548
549 assert_eq!(
550 params.query_pairs(),
551 vec![("bvid", "BV1xx411c7mD".to_string())]
552 );
553 Ok(())
554 }
555
556 #[test]
557 fn video_view_params_serializes_aid_query() -> Result<(), BpiError> {
558 let params = VideoViewParams::from_aid(Aid::new(170001)?);
559
560 assert_eq!(params.query_pairs(), vec![("aid", "170001".to_string())]);
561 Ok(())
562 }
563
564 #[test]
565 fn video_detail_params_serializes_bvid_query_with_electric_flag() -> Result<(), BpiError> {
566 let params = VideoDetailParams::from_bvid("BV1xx411c7mD".parse()?).need_elec(false);
567
568 assert_eq!(
569 params.query_pairs(),
570 vec![
571 ("bvid", "BV1xx411c7mD".to_string()),
572 ("need_elec", "0".to_string()),
573 ]
574 );
575 Ok(())
576 }
577
578 #[test]
579 fn video_page_list_params_serializes_aid_query() -> Result<(), BpiError> {
580 let params = VideoPageListParams::from_aid(Aid::new(170001)?);
581
582 assert_eq!(params.query_pairs(), vec![("aid", "170001".to_string())]);
583 Ok(())
584 }
585
586 #[test]
587 fn video_desc_params_serializes_bvid_query() -> Result<(), BpiError> {
588 let params = VideoDescParams::from_bvid("BV1xx411c7mD".parse()?);
589
590 assert_eq!(
591 params.query_pairs(),
592 vec![("bvid", "BV1xx411c7mD".to_string())]
593 );
594 Ok(())
595 }
596
597 #[test]
598 fn video_play_url_params_serializes_aid_query_with_default_platform() -> Result<(), BpiError> {
599 let params = VideoPlayUrlParams::from_aid(Aid::new(170001)?, Cid::new(180001)?);
600
601 assert_eq!(
602 params.query_pairs(),
603 vec![
604 ("cid", "180001".to_string()),
605 ("avid", "170001".to_string()),
606 ("platform", "pc".to_string())
607 ]
608 );
609 Ok(())
610 }
611
612 #[test]
613 fn video_play_url_params_serializes_optional_playback_flags() -> Result<(), BpiError> {
614 let params = VideoPlayUrlParams::from_bvid("BV1xx411c7mD".parse()?, Cid::new(180001)?)
615 .quality(120)
616 .format_flags(16 | 128)
617 .format_version(0)
618 .fourk(true)
619 .high_quality(true)
620 .try_look(false);
621
622 assert_eq!(
623 params.query_pairs(),
624 vec![
625 ("cid", "180001".to_string()),
626 ("bvid", "BV1xx411c7mD".to_string()),
627 ("qn", "120".to_string()),
628 ("fnval", "144".to_string()),
629 ("fnver", "0".to_string()),
630 ("fourk", "1".to_string()),
631 ("platform", "pc".to_string()),
632 ("high_quality", "1".to_string()),
633 ("try_look", "0".to_string())
634 ]
635 );
636 Ok(())
637 }
638
639 #[test]
640 fn video_online_total_params_serializes_bvid_and_cid_query() -> Result<(), BpiError> {
641 let params = VideoOnlineTotalParams::from_bvid("BV1xx411c7mD".parse()?, Cid::new(62131)?);
642
643 assert_eq!(
644 params.query_pairs(),
645 vec![
646 ("cid", "62131".to_string()),
647 ("bvid", "BV1xx411c7mD".to_string())
648 ]
649 );
650 Ok(())
651 }
652
653 #[test]
654 fn video_player_info_params_serializes_optional_context() -> Result<(), BpiError> {
655 let params = VideoPlayerInfoParams::from_aid(Aid::new(170001)?, Cid::new(180001)?)
656 .season_id(42)?
657 .ep_id(43)?;
658
659 assert_eq!(
660 params.query_pairs(),
661 vec![
662 ("cid", "180001".to_string()),
663 ("aid", "170001".to_string()),
664 ("season_id", "42".to_string()),
665 ("ep_id", "43".to_string())
666 ]
667 );
668 Ok(())
669 }
670
671 #[test]
672 fn video_homepage_recommendations_params_serializes_defaults() {
673 let params = VideoHomepageRecommendationsParams::new();
674
675 assert_eq!(
676 params.query_pairs(),
677 vec![
678 ("fresh_type", "4".to_string()),
679 ("ps", "12".to_string()),
680 ("fresh_idx", "1".to_string()),
681 ("fresh_idx_1h", "1".to_string()),
682 ("brush", "1".to_string()),
683 ("fetch_row", "1".to_string()),
684 ]
685 );
686 }
687
688 #[test]
689 fn video_homepage_recommendations_params_rejects_oversized_page() {
690 let err = VideoHomepageRecommendationsParams::new()
691 .page_size(31)
692 .unwrap_err();
693
694 assert!(matches!(
695 err,
696 BpiError::InvalidParameter { field: "ps", .. }
697 ));
698 }
699
700 #[test]
701 fn video_ai_summary_params_rejects_zero_up_mid() -> Result<(), BpiError> {
702 let err = VideoAiSummaryParams::from_bvid("BV1xx411c7mD".parse()?, Cid::new(62131)?, 0)
703 .unwrap_err();
704
705 assert!(matches!(
706 err,
707 BpiError::InvalidParameter {
708 field: "up_mid",
709 ..
710 }
711 ));
712 Ok(())
713 }
714
715 #[test]
716 fn video_tags_params_serializes_optional_cid() -> Result<(), BpiError> {
717 let params = VideoTagsParams::from_bvid("BV1xx411c7mD".parse()?).cid(Cid::new(62131)?);
718
719 assert_eq!(
720 params.query_pairs(),
721 vec![
722 ("bvid", "BV1xx411c7mD".to_string()),
723 ("cid", "62131".to_string())
724 ]
725 );
726 Ok(())
727 }
728
729 #[test]
730 fn interactive_video_info_params_serializes_start_node() -> Result<(), BpiError> {
731 let params = InteractiveVideoInfoParams::from_aid(Aid::new(114347430905959)?, 1273647)?;
732
733 assert_eq!(
734 params.query_pairs(),
735 vec![
736 ("graph_version", "1273647".to_string()),
737 ("aid", "114347430905959".to_string())
738 ]
739 );
740 Ok(())
741 }
742}