Skip to main content

bpi_rs/video/
params.rs

1use crate::ids::{Aid, Bvid, Cid};
2use crate::{BpiError, BpiResult};
3
4/// Identifies a Bilibili video by either AV numeric ID or BV string ID.
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub enum VideoId {
7    /// AV numeric video ID.
8    Aid(Aid),
9    /// BV string video ID.
10    Bvid(Bvid),
11}
12
13/// Parameters for `/x/web-interface/view`.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct VideoViewParams {
16    id: VideoId,
17}
18
19impl VideoViewParams {
20    /// Creates view parameters from a validated AV ID.
21    pub fn from_aid(aid: Aid) -> Self {
22        Self {
23            id: VideoId::Aid(aid),
24        }
25    }
26
27    /// Creates view parameters from a validated BV ID.
28    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/// Parameters for `/x/web-interface/view/detail`.
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct VideoDetailParams {
42    id: VideoId,
43    need_elec: Option<u8>,
44}
45
46impl VideoDetailParams {
47    /// Creates detail parameters from a validated AV ID.
48    pub fn from_aid(aid: Aid) -> Self {
49        Self::new(VideoId::Aid(aid))
50    }
51
52    /// Creates detail parameters from a validated BV ID.
53    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    /// Controls whether the detail endpoint should include electric charging data.
65    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/// Parameters for `/x/player/pagelist`.
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub struct VideoPageListParams {
84    id: VideoId,
85}
86
87impl VideoPageListParams {
88    /// Creates page-list parameters from a validated AV ID.
89    pub fn from_aid(aid: Aid) -> Self {
90        Self {
91            id: VideoId::Aid(aid),
92        }
93    }
94
95    /// Creates page-list parameters from a validated BV ID.
96    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/// Parameters for `/x/web-interface/archive/desc`.
108#[derive(Debug, Clone, PartialEq, Eq)]
109pub struct VideoDescParams {
110    id: VideoId,
111}
112
113impl VideoDescParams {
114    /// Creates description parameters from a validated AV ID.
115    pub fn from_aid(aid: Aid) -> Self {
116        Self {
117            id: VideoId::Aid(aid),
118        }
119    }
120
121    /// Creates description parameters from a validated BV ID.
122    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/// Parameters for `/x/player/wbi/playurl`.
134#[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    /// Creates play URL parameters from a validated AV ID and page/content ID.
149    pub fn from_aid(aid: Aid, cid: Cid) -> Self {
150        Self::new(VideoId::Aid(aid), cid)
151    }
152
153    /// Creates play URL parameters from a validated BV ID and page/content ID.
154    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    /// Sets the requested quality code.
173    pub fn quality(mut self, qn: u64) -> Self {
174        self.qn = Some(qn);
175        self
176    }
177
178    /// Sets the Bilibili stream format bitmask.
179    pub fn format_flags(mut self, fnval: u64) -> Self {
180        self.fnval = Some(fnval);
181        self
182    }
183
184    /// Sets the stream format version.
185    pub fn format_version(mut self, fnver: u64) -> Self {
186        self.fnver = Some(fnver);
187        self
188    }
189
190    /// Controls whether 4K streams are allowed.
191    pub fn fourk(mut self, enabled: bool) -> Self {
192        self.fourk = Some(u8::from(enabled));
193        self
194    }
195
196    /// Sets the API platform marker. Defaults to `pc`.
197    pub fn platform(mut self, platform: impl Into<String>) -> Self {
198        self.platform = platform.into();
199        self
200    }
201
202    /// Sets the high-quality playback flag.
203    pub fn high_quality(mut self, enabled: bool) -> Self {
204        self.high_quality = Some(u8::from(enabled));
205        self
206    }
207
208    /// Controls whether trial viewing should be requested.
209    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/// Parameters for `/x/player/online/total`.
246#[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/// Parameters for `/x/player/wbi/v2`.
275#[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/// Parameters for `/x/web-interface/archive/related`.
325#[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/// Parameters for `/x/web-interface/wbi/index/top/feed/rcmd`.
349#[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/// Parameters for `/x/web-interface/view/conclusion/get`.
406#[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/// Parameters for `/x/web-interface/view/detail/tag`.
441#[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/// Parameters for `/x/stein/edgeinfo_v2`.
477#[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}