1use crate::audio::musicstream_url::AudioQuality;
2use crate::ids::AudioId;
3use crate::{BpiError, BpiResult};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub struct AudioSongParams {
8 sid: AudioId,
9}
10
11impl AudioSongParams {
12 pub fn new(sid: AudioId) -> Self {
13 Self { sid }
14 }
15
16 pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
17 vec![("sid", self.sid.to_string())]
18 }
19}
20
21#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct AudioCollectionToFavParams {
24 rid: AudioId,
25 add_media_ids: Vec<String>,
26 del_media_ids: Vec<String>,
27}
28
29impl AudioCollectionToFavParams {
30 pub fn new(
31 rid: AudioId,
32 add_media_ids: impl IntoIterator<Item = impl Into<String>>,
33 del_media_ids: impl IntoIterator<Item = impl Into<String>>,
34 ) -> BpiResult<Self> {
35 let add_media_ids = normalize_id_list("add_media_ids", add_media_ids)?;
36 let del_media_ids = normalize_id_list("del_media_ids", del_media_ids)?;
37
38 if add_media_ids.is_empty() && del_media_ids.is_empty() {
39 return Err(BpiError::invalid_parameter(
40 "media_ids",
41 "at least one add or delete media id is required",
42 ));
43 }
44
45 Ok(Self {
46 rid,
47 add_media_ids,
48 del_media_ids,
49 })
50 }
51
52 pub(crate) fn form_pairs(&self, csrf: &str) -> Vec<(&'static str, String)> {
53 let mut pairs = vec![
54 ("rid", self.rid.to_string()),
55 ("type", "12".to_string()),
56 ("csrf", csrf.to_string()),
57 ];
58
59 if !self.add_media_ids.is_empty() {
60 pairs.push(("add_media_ids", self.add_media_ids.join(",")));
61 }
62 if !self.del_media_ids.is_empty() {
63 pairs.push(("del_media_ids", self.del_media_ids.join(",")));
64 }
65
66 pairs
67 }
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub struct AudioCollectionToParams {
73 sid: AudioId,
74 cids: u64,
75}
76
77impl AudioCollectionToParams {
78 pub fn new(sid: AudioId, cids: u64) -> BpiResult<Self> {
79 Ok(Self {
80 sid,
81 cids: validate_nonzero("cids", cids)?,
82 })
83 }
84
85 pub(crate) fn form_pairs(&self, csrf: &str) -> Vec<(&'static str, String)> {
86 vec![
87 ("sid", self.sid.to_string()),
88 ("cids", self.cids.to_string()),
89 ("csrf", csrf.to_string()),
90 ]
91 }
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub struct AudioCoinParams {
97 sid: AudioId,
98 multiply: u32,
99}
100
101impl AudioCoinParams {
102 pub fn new(sid: AudioId, multiply: u32) -> BpiResult<Self> {
103 Ok(Self {
104 sid,
105 multiply: validate_coin_multiply(multiply)?,
106 })
107 }
108
109 pub(crate) fn form_pairs(&self, csrf: &str) -> Vec<(&'static str, String)> {
110 vec![
111 ("sid", self.sid.to_string()),
112 ("multiply", self.multiply.to_string()),
113 ("csrf", csrf.to_string()),
114 ]
115 }
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120pub struct AudioPageParams {
121 page: u32,
122 page_size: u32,
123}
124
125impl AudioPageParams {
126 pub fn new(page: u32, page_size: u32) -> BpiResult<Self> {
127 Ok(Self {
128 page: validate_nonzero("pn", page)?,
129 page_size: validate_nonzero("ps", page_size)?,
130 })
131 }
132
133 pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
134 vec![
135 ("pn", self.page.to_string()),
136 ("ps", self.page_size.to_string()),
137 ]
138 }
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143pub struct AudioCollectionInfoParams {
144 sid: u64,
145}
146
147impl AudioCollectionInfoParams {
148 pub fn new(sid: u64) -> BpiResult<Self> {
149 Ok(Self {
150 sid: validate_nonzero("sid", sid)?,
151 })
152 }
153
154 pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
155 vec![("sid", self.sid.to_string())]
156 }
157}
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161pub enum AudioRankListType {
162 Hot,
163 Original,
164 Custom(u32),
165}
166
167impl AudioRankListType {
168 fn query_value(self) -> String {
169 match self {
170 Self::Hot => "1".to_string(),
171 Self::Original => "2".to_string(),
172 Self::Custom(value) => value.to_string(),
173 }
174 }
175}
176
177#[derive(Debug, Clone, Copy, PartialEq, Eq)]
179pub struct AudioRankPeriodParams {
180 list_type: AudioRankListType,
181}
182
183impl AudioRankPeriodParams {
184 pub fn new(list_type: AudioRankListType) -> Self {
185 Self { list_type }
186 }
187
188 pub fn custom(list_type: u32) -> BpiResult<Self> {
189 Ok(Self {
190 list_type: AudioRankListType::Custom(validate_nonzero("list_type", list_type)?),
191 })
192 }
193
194 pub(crate) fn query_pairs(&self, csrf: &str) -> Vec<(&'static str, String)> {
195 vec![
196 ("list_type", self.list_type.query_value()),
197 ("csrf", csrf.to_string()),
198 ]
199 }
200}
201
202#[derive(Debug, Clone, Copy, PartialEq, Eq)]
204pub struct AudioRankListParams {
205 list_id: u64,
206}
207
208impl AudioRankListParams {
209 pub fn new(list_id: u64) -> BpiResult<Self> {
210 Ok(Self {
211 list_id: validate_nonzero("list_id", list_id)?,
212 })
213 }
214
215 pub(crate) fn query_pairs(&self, csrf: &str) -> Vec<(&'static str, String)> {
216 vec![
217 ("list_id", self.list_id.to_string()),
218 ("csrf", csrf.to_string()),
219 ]
220 }
221}
222
223#[derive(Debug, Clone, Copy, PartialEq, Eq)]
225pub struct AudioStreamUrlWebParams {
226 sid: AudioId,
227 quality: AudioQuality,
228 privilege: u32,
229}
230
231impl AudioStreamUrlWebParams {
232 pub fn new(sid: AudioId) -> Self {
233 Self {
234 sid,
235 quality: AudioQuality::HighQuality,
236 privilege: 2,
237 }
238 }
239
240 pub fn with_quality(mut self, quality: AudioQuality) -> Self {
241 self.quality = quality;
242 self
243 }
244
245 pub fn with_privilege(mut self, privilege: u32) -> BpiResult<Self> {
246 self.privilege = validate_nonzero("privilege", privilege)?;
247 Ok(self)
248 }
249
250 pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
251 vec![
252 ("sid", self.sid.to_string()),
253 ("quality", self.quality.as_u32().to_string()),
254 ("privilege", self.privilege.to_string()),
255 ]
256 }
257}
258
259#[derive(Debug, Clone, PartialEq, Eq)]
261pub struct AudioStreamUrlParams {
262 song_id: AudioId,
263 quality: AudioQuality,
264 privilege: u32,
265 mid: u64,
266 platform: String,
267}
268
269impl AudioStreamUrlParams {
270 pub fn new(song_id: AudioId, quality: AudioQuality) -> Self {
271 Self {
272 song_id,
273 quality,
274 privilege: 2,
275 mid: 2,
276 platform: "android".to_string(),
277 }
278 }
279
280 pub fn with_privilege(mut self, privilege: u32) -> BpiResult<Self> {
281 self.privilege = validate_nonzero("privilege", privilege)?;
282 Ok(self)
283 }
284
285 pub fn with_mid(mut self, mid: u64) -> BpiResult<Self> {
286 self.mid = validate_nonzero("mid", mid)?;
287 Ok(self)
288 }
289
290 pub fn with_platform(mut self, platform: impl Into<String>) -> BpiResult<Self> {
291 let platform = platform.into();
292 let platform = platform.trim();
293 if platform.is_empty() {
294 return Err(BpiError::invalid_parameter(
295 "platform",
296 "value cannot be blank",
297 ));
298 }
299
300 self.platform = platform.to_string();
301 Ok(self)
302 }
303
304 pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
305 vec![
306 ("songid", self.song_id.to_string()),
307 ("quality", self.quality.as_u32().to_string()),
308 ("privilege", self.privilege.to_string()),
309 ("mid", self.mid.to_string()),
310 ("platform", self.platform.clone()),
311 ]
312 }
313}
314
315fn validate_nonzero<T>(field: &'static str, value: T) -> BpiResult<T>
316where
317 T: PartialEq + From<u8>,
318{
319 if value == T::from(0) {
320 return Err(BpiError::invalid_parameter(field, "value must be non-zero"));
321 }
322
323 Ok(value)
324}
325
326fn validate_coin_multiply(value: u32) -> BpiResult<u32> {
327 if matches!(value, 1 | 2) {
328 return Ok(value);
329 }
330
331 Err(BpiError::invalid_parameter(
332 "multiply",
333 "value must be 1 or 2",
334 ))
335}
336
337fn normalize_id_list(
338 field: &'static str,
339 values: impl IntoIterator<Item = impl Into<String>>,
340) -> BpiResult<Vec<String>> {
341 values
342 .into_iter()
343 .map(|value| {
344 let value = value.into();
345 let value = value.trim();
346 if value.is_empty() {
347 return Err(BpiError::invalid_parameter(field, "value cannot be blank"));
348 }
349
350 Ok(value.to_string())
351 })
352 .collect()
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358
359 #[test]
360 fn audio_song_params_serializes_sid_query() -> BpiResult<()> {
361 let params = AudioSongParams::new(AudioId::new(13603)?);
362
363 assert_eq!(params.query_pairs(), vec![("sid", "13603".to_string())]);
364 Ok(())
365 }
366
367 #[test]
368 fn audio_song_params_accepts_owned_audio_id() -> BpiResult<()> {
369 let sid = AudioId::new(15664)?;
370 let params = AudioSongParams::new(sid);
371
372 assert_eq!(params.query_pairs(), vec![("sid", "15664".to_string())]);
373 Ok(())
374 }
375
376 #[test]
377 fn audio_collection_to_fav_params_requires_an_operation() -> BpiResult<()> {
378 let err = AudioCollectionToFavParams::new(
379 AudioId::new(13603)?,
380 Vec::<String>::new(),
381 Vec::<String>::new(),
382 )
383 .unwrap_err();
384
385 assert!(matches!(
386 err,
387 BpiError::InvalidParameter {
388 field: "media_ids",
389 ..
390 }
391 ));
392 Ok(())
393 }
394
395 #[test]
396 fn audio_collection_to_params_rejects_zero_collection_id() -> BpiResult<()> {
397 let err = AudioCollectionToParams::new(AudioId::new(13603)?, 0).unwrap_err();
398
399 assert!(matches!(
400 err,
401 BpiError::InvalidParameter { field: "cids", .. }
402 ));
403 Ok(())
404 }
405
406 #[test]
407 fn audio_coin_params_rejects_invalid_multiply() -> BpiResult<()> {
408 let err = AudioCoinParams::new(AudioId::new(13603)?, 0).unwrap_err();
409
410 assert!(matches!(
411 err,
412 BpiError::InvalidParameter {
413 field: "multiply",
414 ..
415 }
416 ));
417 Ok(())
418 }
419
420 #[test]
421 fn audio_page_params_serializes_page_query() -> BpiResult<()> {
422 let params = AudioPageParams::new(1, 20)?;
423
424 assert_eq!(
425 params.query_pairs(),
426 vec![("pn", "1".to_string()), ("ps", "20".to_string())]
427 );
428 Ok(())
429 }
430
431 #[test]
432 fn audio_page_params_rejects_zero_page() {
433 let err = AudioPageParams::new(0, 20).unwrap_err();
434
435 assert!(matches!(
436 err,
437 BpiError::InvalidParameter { field: "pn", .. }
438 ));
439 }
440
441 #[test]
442 fn audio_collection_info_params_serializes_collection_id_query() -> BpiResult<()> {
443 let params = AudioCollectionInfoParams::new(15_967_839)?;
444
445 assert_eq!(params.query_pairs(), vec![("sid", "15967839".to_string())]);
446 Ok(())
447 }
448
449 #[test]
450 fn audio_rank_period_params_serializes_builtin_list_type() {
451 let params = AudioRankPeriodParams::new(AudioRankListType::Original);
452
453 assert_eq!(
454 params.query_pairs("csrf-token"),
455 vec![
456 ("list_type", "2".to_string()),
457 ("csrf", "csrf-token".to_string())
458 ]
459 );
460 }
461
462 #[test]
463 fn audio_rank_period_params_rejects_zero_custom_type() {
464 let err = AudioRankPeriodParams::custom(0).unwrap_err();
465
466 assert!(matches!(
467 err,
468 BpiError::InvalidParameter {
469 field: "list_type",
470 ..
471 }
472 ));
473 }
474
475 #[test]
476 fn audio_rank_list_params_serializes_list_id_query() -> BpiResult<()> {
477 let params = AudioRankListParams::new(76)?;
478
479 assert_eq!(
480 params.query_pairs("csrf-token"),
481 vec![
482 ("list_id", "76".to_string()),
483 ("csrf", "csrf-token".to_string())
484 ]
485 );
486 Ok(())
487 }
488
489 #[test]
490 fn audio_stream_url_web_params_serializes_default_query() -> BpiResult<()> {
491 let params = AudioStreamUrlWebParams::new(AudioId::new(13603)?);
492
493 assert_eq!(
494 params.query_pairs(),
495 vec![
496 ("sid", "13603".to_string()),
497 ("quality", "2".to_string()),
498 ("privilege", "2".to_string()),
499 ]
500 );
501 Ok(())
502 }
503
504 #[test]
505 fn audio_stream_url_params_serializes_default_query() -> BpiResult<()> {
506 let params = AudioStreamUrlParams::new(AudioId::new(15664)?, AudioQuality::HighQuality);
507
508 assert_eq!(
509 params.query_pairs(),
510 vec![
511 ("songid", "15664".to_string()),
512 ("quality", "2".to_string()),
513 ("privilege", "2".to_string()),
514 ("mid", "2".to_string()),
515 ("platform", "android".to_string()),
516 ]
517 );
518 Ok(())
519 }
520
521 #[test]
522 fn audio_stream_url_params_serializes_custom_query() -> BpiResult<()> {
523 let params = AudioStreamUrlParams::new(AudioId::new(15664)?, AudioQuality::Lossless)
524 .with_privilege(3)?
525 .with_mid(42)?
526 .with_platform("ios")?;
527
528 assert_eq!(
529 params.query_pairs(),
530 vec![
531 ("songid", "15664".to_string()),
532 ("quality", "3".to_string()),
533 ("privilege", "3".to_string()),
534 ("mid", "42".to_string()),
535 ("platform", "ios".to_string()),
536 ]
537 );
538 Ok(())
539 }
540
541 #[test]
542 fn audio_stream_url_params_rejects_blank_platform() {
543 let err = AudioStreamUrlParams::new(
544 AudioId::new(15664).expect("valid audio id"),
545 AudioQuality::HighQuality,
546 )
547 .with_platform(" ")
548 .unwrap_err();
549
550 assert!(matches!(
551 err,
552 BpiError::InvalidParameter {
553 field: "platform",
554 ..
555 }
556 ));
557 }
558}