Skip to main content

baidu_netdisk_sdk/playlist/
mod.rs

1//! Playlist and media playback functionality
2//!
3//! This module provides access to Baidu NetDisk's playlist and media streaming features:
4//!
5//! # Features
6//!
7//! - **Playlist management**: List playlists and their contents
8//! - **Media streaming**: Get media playback information and m3u8 streams
9//! - **Quality selection**: Video/Audio quality enums with VIP level support
10//! - **Transcoding check**: Verify if media is fully transcoded
11//!
12//! # Quick Start
13//!
14//! ```
15//! use baidu_netdisk_sdk::{BaiduNetDiskClient, playlist::VideoQuality};
16//!
17//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
18//! // Create client and load token
19//! let client = BaiduNetDiskClient::builder().build()?;
20//! client.load_token_from_env()?;
21//!
22//! // List all playlists
23//! let playlists = client.playlist().get_playlist_list().await?;
24//!
25//! // Get media m3u8 with highest quality for VIP 2
26//! let m3u8 = client.playlist()
27//!     .get_video_m3u8_highest("/video.mp4", 2)
28//!     .await?;
29//! # Ok(())
30//! # }
31//! ```
32
33use log::{debug, error, info};
34use serde::{Deserialize, Serialize};
35use std::sync::Arc;
36
37use crate::client::TokenGetter;
38use crate::errors::{NetDiskError, NetDiskResult};
39use crate::http::HttpClient;
40
41/// Video quality levels for m3u8 streaming
42///
43/// # Examples
44///
45/// ```
46/// use baidu_netdisk_sdk::playlist::VideoQuality;
47///
48/// let quality = VideoQuality::Quality1080P;
49/// assert_eq!(quality.to_media_type(), "M3U8_AUTO_1080");
50///
51/// // Get highest quality for VIP level
52/// let quality = VideoQuality::highest_for_vip_level(2);
53/// assert_eq!(quality, VideoQuality::Quality1080P);
54/// ```
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum VideoQuality {
57    /// 480P quality - available to all users
58    Quality480P,
59    /// 720P quality - highest for regular users
60    Quality720P,
61    /// 1080P quality - usually requires super VIP
62    Quality1080P,
63}
64
65impl VideoQuality {
66    /// Get the corresponding media_type string for API
67    ///
68    /// # Examples
69    ///
70    /// ```
71    /// use baidu_netdisk_sdk::playlist::VideoQuality;
72    ///
73    /// assert_eq!(VideoQuality::Quality480P.to_media_type(), "M3U8_AUTO_480");
74    /// assert_eq!(VideoQuality::Quality720P.to_media_type(), "M3U8_AUTO_720");
75    /// assert_eq!(VideoQuality::Quality1080P.to_media_type(), "M3U8_AUTO_1080");
76    /// ```
77    pub fn to_media_type(self) -> &'static str {
78        match self {
79            VideoQuality::Quality480P => "M3U8_AUTO_480",
80            VideoQuality::Quality720P => "M3U8_AUTO_720",
81            VideoQuality::Quality1080P => "M3U8_AUTO_1080",
82        }
83    }
84
85    /// Get the highest quality available for a given VIP level
86    ///
87    /// - **VIP 0-1**: Max 480P
88    /// - **VIP 2+**: Max 1080P (includes 720P)
89    ///
90    /// # Examples
91    ///
92    /// ```
93    /// use baidu_netdisk_sdk::playlist::VideoQuality;
94    ///
95    /// assert_eq!(VideoQuality::highest_for_vip_level(0), VideoQuality::Quality480P);
96    /// assert_eq!(VideoQuality::highest_for_vip_level(2), VideoQuality::Quality1080P);
97    /// ```
98    pub fn highest_for_vip_level(vip_level: u32) -> Self {
99        match vip_level {
100            0 | 1 => VideoQuality::Quality480P,
101            _ => VideoQuality::Quality1080P,
102        }
103    }
104
105    /// Get all qualities available for a given VIP level (from lowest to highest)
106    ///
107    /// # Examples
108    ///
109    /// ```
110    /// use baidu_netdisk_sdk::playlist::VideoQuality;
111    ///
112    /// let qualities = VideoQuality::available_for_vip_level(2);
113    /// assert_eq!(qualities.len(), 3);
114    /// assert_eq!(qualities[0], VideoQuality::Quality480P);
115    /// assert_eq!(qualities[2], VideoQuality::Quality1080P);
116    /// ```
117    pub fn available_for_vip_level(vip_level: u32) -> Vec<Self> {
118        match vip_level {
119            0 | 1 => vec![VideoQuality::Quality480P],
120            _ => vec![
121                VideoQuality::Quality480P,
122                VideoQuality::Quality720P,
123                VideoQuality::Quality1080P,
124            ],
125        }
126    }
127}
128
129/// Audio quality levels for m3u8 streaming
130///
131/// # Examples
132///
133/// ```
134/// use baidu_netdisk_sdk::playlist::AudioQuality;
135///
136/// let quality = AudioQuality::Quality128K;
137/// assert_eq!(quality.to_media_type(), "M3U8_MP3_128");
138/// ```
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140pub enum AudioQuality {
141    /// 128kbps MP3
142    Quality128K,
143}
144
145impl AudioQuality {
146    /// Get the corresponding media_type string for API
147    ///
148    /// # Examples
149    ///
150    /// ```
151    /// use baidu_netdisk_sdk::playlist::AudioQuality;
152    ///
153    /// assert_eq!(AudioQuality::Quality128K.to_media_type(), "M3U8_MP3_128");
154    /// ```
155    pub fn to_media_type(self) -> &'static str {
156        match self {
157            AudioQuality::Quality128K => "M3U8_MP3_128",
158        }
159    }
160}
161
162/// Playlist client for interacting with playlist-related APIs
163///
164/// # Examples
165///
166/// ```
167/// use baidu_netdisk_sdk::BaiduNetDiskClient;
168///
169/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
170/// let client = BaiduNetDiskClient::builder().build()?;
171/// client.load_token_from_env()?;
172///
173/// // Access playlist functionality
174/// let playlists = client.playlist().get_playlist_list().await?;
175/// # Ok(())
176/// # }
177/// ```
178#[derive(Debug, Clone)]
179pub struct PlaylistClient {
180    http_client: HttpClient,
181    token_getter: Arc<dyn TokenGetter>,
182}
183
184impl PlaylistClient {
185    /// Create a new PlaylistClient instance
186    ///
187    /// Usually you don't need to call this directly - use `BaiduNetDiskClient::playlist()` instead.
188    pub fn new(http_client: HttpClient, token_getter: Arc<dyn TokenGetter>) -> Self {
189        PlaylistClient {
190            http_client,
191            token_getter,
192        }
193    }
194
195    /// Get a reference to the internal HTTP client
196    pub fn http_client(&self) -> &HttpClient {
197        &self.http_client
198    }
199
200    /// Get a list of playlists with default options
201    ///
202    /// # Examples
203    ///
204    /// ```
205    /// use baidu_netdisk_sdk::BaiduNetDiskClient;
206    ///
207    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
208    /// let client = BaiduNetDiskClient::builder().build()?;
209    /// client.load_token_from_env()?;
210    ///
211    /// let playlists = client.playlist().get_playlist_list().await?;
212    /// println!("Found {} playlists", playlists.list.len());
213    /// # Ok(())
214    /// # }
215    /// ```
216    pub async fn get_playlist_list(&self) -> NetDiskResult<PlaylistList> {
217        self.get_playlist_list_with_options(PlaylistListOptions::default())
218            .await
219    }
220
221    /// Get a list of playlists with custom options
222    ///
223    /// This is a lower-level method for advanced use cases.
224    /// Most users should use `get_playlist_list()` instead.
225    pub async fn get_playlist_list_with_options(
226        &self,
227        options: PlaylistListOptions,
228    ) -> NetDiskResult<PlaylistList> {
229        let token = self.token_getter.get_token().await?;
230        let mut params = Vec::new();
231
232        params.push(("method", "list".to_string()));
233        params.push(("access_token", token.access_token.clone()));
234
235        if let Some(p) = options.page {
236            params.push(("page", p.to_string()));
237        }
238        if let Some(s) = options.psize {
239            params.push(("psize", s.to_string()));
240        }
241
242        let params_ref: Vec<(&str, &str)> = params.iter().map(|(k, v)| (*k, v.as_str())).collect();
243
244        debug!("Getting playlist list with options: {:?}", options);
245
246        let response: PlaylistListResponse = self
247            .http_client
248            .get("/rest/2.0/xpan/broadcast/list", Some(&params_ref))
249            .await?;
250
251        if response.errno != 0 {
252            let errmsg = response.errmsg.as_deref().unwrap_or("Unknown error");
253            return Err(NetDiskError::api_error(response.errno, errmsg));
254        }
255
256        let list = response.list.unwrap_or_default();
257        info!(
258            "Playlist list retrieved successfully, count: {}",
259            list.len()
260        );
261
262        Ok(PlaylistList {
263            has_more: response.has_more,
264            list,
265        })
266    }
267
268    /// Get playlist file download list with default options
269    ///
270    /// # Examples
271    ///
272    /// ```
273    /// use baidu_netdisk_sdk::BaiduNetDiskClient;
274    ///
275    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
276    /// let client = BaiduNetDiskClient::builder().build()?;
277    /// client.load_token_from_env()?;
278    ///
279    /// // First get playlists to find mb_id
280    /// let playlists = client.playlist().get_playlist_list().await?;
281    /// if let Some(playlist) = playlists.list.first() {
282    ///     // Then get files in playlist
283    ///     let files = client.playlist()
284    ///         .get_playlist_file_list(playlist.mb_id)
285    ///         .await?;
286    ///     println!("Found {} files", files.list.len());
287    /// }
288    /// # Ok(())
289    /// # }
290    /// ```
291    pub async fn get_playlist_file_list(&self, mb_id: u64) -> NetDiskResult<PlaylistFileList> {
292        self.get_playlist_file_list_with_options(mb_id, PlaylistFileListOptions::default())
293            .await
294    }
295
296    /// Get playlist file download list with custom options
297    ///
298    /// This is a lower-level method for advanced use cases.
299    /// Most users should use `get_playlist_file_list()` instead.
300    pub async fn get_playlist_file_list_with_options(
301        &self,
302        mb_id: u64,
303        options: PlaylistFileListOptions,
304    ) -> NetDiskResult<PlaylistFileList> {
305        let token = self.token_getter.get_token().await?;
306        let mut params = Vec::new();
307
308        params.push(("access_token", token.access_token.clone()));
309        params.push(("mb_id", mb_id.to_string()));
310
311        if let Some(s) = options.showmeta {
312            params.push(("showmeta", s.to_string()));
313        }
314        if let Some(p) = options.page {
315            params.push(("page", p.to_string()));
316        }
317        if let Some(s) = options.psize {
318            params.push(("psize", s.to_string()));
319        }
320
321        let params_ref: Vec<(&str, &str)> = params.iter().map(|(k, v)| (*k, v.as_str())).collect();
322
323        debug!(
324            "Getting playlist file list for mb_id: {} with options: {:?}",
325            mb_id, options
326        );
327
328        let response: PlaylistFileListResponse = self
329            .http_client
330            .post("/rest/2.0/xpan/broadcast/filelist", Some(&params_ref))
331            .await?;
332
333        if response.errno != 0 {
334            let errmsg = response.errmsg.as_deref().unwrap_or("Unknown error");
335            return Err(NetDiskError::api_error(response.errno, errmsg));
336        }
337
338        let list = response.list.unwrap_or_default();
339        info!(
340            "Playlist file list retrieved successfully, count: {}",
341            list.len()
342        );
343
344        Ok(PlaylistFileList {
345            has_more: response.has_more,
346            list,
347        })
348    }
349
350    /// Get media playback information (audio or video)
351    ///
352    /// Takes either fs_id or path (one of them is required)
353    ///
354    /// # Examples
355    ///
356    /// ```
357    /// use baidu_netdisk_sdk::BaiduNetDiskClient;
358    ///
359    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
360    /// let client = BaiduNetDiskClient::builder().build()?;
361    /// client.load_token_from_env()?;
362    ///
363    /// // Get by path
364    /// let info = client.playlist()
365    ///     .get_media_play_info(None, Some("/video.mp4"), "M3U8_AUTO_1080")
366    ///     .await?;
367    ///
368    /// // Or get by fs_id
369    /// let info = client.playlist()
370    ///     .get_media_play_info(Some(123456), None, "M3U8_AUTO_1080")
371    ///     .await?;
372    /// # Ok(())
373    /// # }
374    /// ```
375    pub async fn get_media_play_info(
376        &self,
377        fsid: Option<u64>,
378        path: Option<&str>,
379        media_type: &str,
380    ) -> NetDiskResult<MediaPlayInfo> {
381        let token = self.token_getter.get_token().await?;
382        let mut params = Vec::new();
383
384        params.push(("method", "streaming".to_string()));
385        params.push(("access_token", token.access_token.clone()));
386        params.push(("type", media_type.to_string()));
387
388        if let Some(f) = fsid {
389            params.push(("fid", f.to_string()));
390        }
391        if let Some(p) = path {
392            params.push(("path", p.to_string()));
393        }
394
395        let params_ref: Vec<(&str, &str)> = params.iter().map(|(k, v)| (*k, v.as_str())).collect();
396
397        let headers = [
398            (
399                "User-Agent",
400                "xpanvideo;netdisk;iPhone13;ios-iphone;15.1;ts",
401            ),
402            ("Host", "pan.baidu.com"),
403            ("Accept", "*/*"),
404            ("Accept-Language", "zh-CN,zh;q=0.9"),
405        ];
406
407        debug!("Getting media play info with params: {:?}", params_ref);
408
409        let response: MediaPlayInfoResponse = self
410            .http_client
411            .get_with_headers("/rest/2.0/xpan/file", Some(&params_ref), Some(&headers))
412            .await?;
413
414        if response.errno != 0 {
415            let errmsg = response.errmsg.as_deref().unwrap_or("Unknown error");
416            return Err(NetDiskError::api_error(response.errno, errmsg));
417        }
418
419        let list = response.list.unwrap_or_default();
420        info!("Media play info retrieved successfully");
421
422        Ok(MediaPlayInfo {
423            list,
424            request_id: response.request_id,
425        })
426    }
427
428    /// Fetch m3u8 playlist content with special headers
429    ///
430    /// This is for checking if the media is fully transcoded
431    ///
432    /// # Examples
433    ///
434    /// ```
435    /// use baidu_netdisk_sdk::BaiduNetDiskClient;
436    ///
437    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
438    /// let client = BaiduNetDiskClient::builder().build()?;
439    ///
440    /// // First get play info to get m3u8_url
441    /// // Then fetch the content
442    /// // let content = client.playlist().fetch_m3u8(m3u8_url).await?;
443    /// # Ok(())
444    /// # }
445    /// ```
446    pub async fn fetch_m3u8(&self, m3u8_url: &str) -> NetDiskResult<String> {
447        debug!("Fetching m3u8 content from: {}", m3u8_url);
448
449        let client = reqwest::Client::new();
450        let response = client
451            .get(m3u8_url)
452            .header(
453                "User-Agent",
454                "xpanvideo;netdisk;iPhone13;ios-iphone;15.1;ts",
455            )
456            .header("Host", "pan.baidu.com")
457            .send()
458            .await?;
459
460        if !response.status().is_success() {
461            return Err(NetDiskError::Unknown {
462                message: format!("Failed to fetch m3u8: {}", response.status()),
463            });
464        }
465
466        let content = response.text().await?;
467        debug!(
468            "Successfully fetched m3u8 content, length: {}",
469            content.len()
470        );
471
472        Ok(content)
473    }
474
475    /// Check if media is fully transcoded by checking if m3u8 contains #EXT-X-ENDLIST
476    ///
477    /// # Examples
478    ///
479    /// ```
480    /// use baidu_netdisk_sdk::BaiduNetDiskClient;
481    ///
482    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
483    /// let client = BaiduNetDiskClient::builder().build()?;
484    ///
485    /// // Check if transcoding is complete
486    /// // let is_complete = client.playlist().is_media_fully_transcoded(m3u8_url).await?;
487    /// # Ok(())
488    /// # }
489    /// ```
490    pub async fn is_media_fully_transcoded(&self, m3u8_url: &str) -> NetDiskResult<bool> {
491        let content = self.fetch_m3u8(m3u8_url).await?;
492        Ok(content.contains("#EXT-X-ENDLIST"))
493    }
494
495    /// Get raw m3u8 content for media file by path
496    ///
497    /// This returns the raw m3u8 playlist content. It does NOT poll for completion.
498    /// Use `is_media_fully_transcoded` or implement your own polling if needed.
499    ///
500    /// # Examples
501    ///
502    /// ```
503    /// use baidu_netdisk_sdk::BaiduNetDiskClient;
504    ///
505    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
506    /// let client = BaiduNetDiskClient::builder().build()?;
507    /// client.load_token_from_env()?;
508    ///
509    /// let m3u8 = client.playlist()
510    ///     .get_media_m3u8_content("/video.mp4", "M3U8_AUTO_1080")
511    ///     .await?;
512    ///
513    /// // Check if fully transcoded
514    /// let is_complete = m3u8.contains("#EXT-X-ENDLIST");
515    /// # Ok(())
516    /// # }
517    /// ```
518    pub async fn get_media_m3u8_content(
519        &self,
520        path: &str,
521        media_type: &str,
522    ) -> NetDiskResult<String> {
523        let token = self.token_getter.get_token().await?;
524        let params = [
525            ("method", "streaming".to_string()),
526            ("access_token", token.access_token.clone()),
527            ("path", path.to_string()),
528            ("type", media_type.to_string()),
529        ];
530
531        let params_ref: Vec<(&str, &str)> = params.iter().map(|(k, v)| (*k, v.as_str())).collect();
532
533        debug!(
534            "Getting raw m3u8 content for path: {}, type: {}",
535            path, media_type
536        );
537
538        let mut url = reqwest::Url::parse("https://pan.baidu.com/rest/2.0/xpan/file")?;
539        {
540            let mut pairs = url.query_pairs_mut();
541            for (key, value) in &params_ref {
542                pairs.append_pair(key, value);
543            }
544        }
545
546        debug!("Built URL for m3u8: {}", url);
547
548        let client = reqwest::Client::new();
549        let response = client
550            .get(url.clone())
551            .header(
552                "User-Agent",
553                "xpanvideo;netdisk;iPhone13;ios-iphone;15.1;ts",
554            )
555            .header("Host", "pan.baidu.com")
556            .header("Accept", "*/*")
557            .header("Accept-Language", "zh-CN,zh;q=0.9")
558            .send()
559            .await?;
560
561        if !response.status().is_success() {
562            let status = response.status();
563            let body = response.text().await.unwrap_or_default();
564            error!("Failed to get m3u8 content: {} - {}", status, body);
565            return Err(NetDiskError::http_error(status.as_u16(), url.as_ref()));
566        }
567
568        let content = response.text().await?;
569        debug!(
570            "Successfully fetched m3u8 content, length: {}",
571            content.len()
572        );
573
574        Ok(content)
575    }
576
577    // Convenience methods using quality enums
578
579    /// Get video m3u8 content with specified quality
580    ///
581    /// # Examples
582    ///
583    /// ```
584    /// use baidu_netdisk_sdk::BaiduNetDiskClient;
585    /// use baidu_netdisk_sdk::playlist::VideoQuality;
586    ///
587    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
588    /// let client = BaiduNetDiskClient::builder().build()?;
589    /// client.load_token_from_env()?;
590    ///
591    /// let m3u8 = client.playlist()
592    ///     .get_video_m3u8("/video.mp4", VideoQuality::Quality1080P)
593    ///     .await?;
594    /// # Ok(())
595    /// # }
596    /// ```
597    pub async fn get_video_m3u8(&self, path: &str, quality: VideoQuality) -> NetDiskResult<String> {
598        self.get_media_m3u8_content(path, quality.to_media_type())
599            .await
600    }
601
602    /// Get video m3u8 content with highest available quality for given VIP level
603    ///
604    /// # Examples
605    ///
606    /// ```
607    /// use baidu_netdisk_sdk::BaiduNetDiskClient;
608    ///
609    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
610    /// let client = BaiduNetDiskClient::builder().build()?;
611    /// client.load_token_from_env()?;
612    ///
613    /// // Get highest quality for VIP 2 (1080P)
614    /// let m3u8 = client.playlist()
615    ///     .get_video_m3u8_highest("/video.mp4", 2)
616    ///     .await?;
617    /// # Ok(())
618    /// # }
619    /// ```
620    pub async fn get_video_m3u8_highest(
621        &self,
622        path: &str,
623        vip_level: u32,
624    ) -> NetDiskResult<String> {
625        let quality = VideoQuality::highest_for_vip_level(vip_level);
626        self.get_video_m3u8(path, quality).await
627    }
628
629    /// Get audio m3u8 content with specified quality
630    ///
631    /// # Examples
632    ///
633    /// ```
634    /// use baidu_netdisk_sdk::BaiduNetDiskClient;
635    /// use baidu_netdisk_sdk::playlist::AudioQuality;
636    ///
637    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
638    /// let client = BaiduNetDiskClient::builder().build()?;
639    /// client.load_token_from_env()?;
640    ///
641    /// let m3u8 = client.playlist()
642    ///     .get_audio_m3u8("/audio.mp3", AudioQuality::Quality128K)
643    ///     .await?;
644    /// # Ok(())
645    /// # }
646    /// ```
647    pub async fn get_audio_m3u8(&self, path: &str, quality: AudioQuality) -> NetDiskResult<String> {
648        self.get_media_m3u8_content(path, quality.to_media_type())
649            .await
650    }
651
652    /// Get audio m3u8 content with default quality (128K)
653    ///
654    /// # Examples
655    ///
656    /// ```
657    /// use baidu_netdisk_sdk::BaiduNetDiskClient;
658    ///
659    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
660    /// let client = BaiduNetDiskClient::builder().build()?;
661    /// client.load_token_from_env()?;
662    ///
663    /// let m3u8 = client.playlist()
664    ///     .get_audio_m3u8_default("/audio.mp3")
665    ///     .await?;
666    /// # Ok(())
667    /// # }
668    /// ```
669    pub async fn get_audio_m3u8_default(&self, path: &str) -> NetDiskResult<String> {
670        self.get_audio_m3u8(path, AudioQuality::Quality128K).await
671    }
672}
673
674/// Options for get_playlist_list
675#[derive(Debug, Deserialize, Serialize, Clone, Default)]
676pub struct PlaylistListOptions {
677    /// Current page number (default 1)
678    pub page: Option<i32>,
679    /// Number of items per page (default 20)
680    pub psize: Option<i32>,
681}
682
683impl PlaylistListOptions {
684    /// Create a new PlaylistListOptions with default values
685    pub fn new() -> Self {
686        Self::default()
687    }
688
689    /// Set page number
690    pub fn page(mut self, page: i32) -> Self {
691        self.page = Some(page);
692        self
693    }
694
695    /// Set page size
696    pub fn psize(mut self, psize: i32) -> Self {
697        self.psize = Some(psize);
698        self
699    }
700}
701
702/// Options for get_playlist_file_list
703#[derive(Debug, Deserialize, Serialize, Clone, Default)]
704pub struct PlaylistFileListOptions {
705    /// Show file details (1 or 0)
706    pub showmeta: Option<i32>,
707    /// Current page number (default 1)
708    pub page: Option<i32>,
709    /// Number of items per page (default 20)
710    pub psize: Option<i32>,
711}
712
713impl PlaylistFileListOptions {
714    /// Create a new PlaylistFileListOptions with default values
715    pub fn new() -> Self {
716        Self::default()
717    }
718
719    /// Set showmeta flag (1 to show details, 0 otherwise)
720    pub fn showmeta(mut self, showmeta: i32) -> Self {
721        self.showmeta = Some(showmeta);
722        self
723    }
724
725    /// Set page number
726    pub fn page(mut self, page: i32) -> Self {
727        self.page = Some(page);
728        self
729    }
730
731    /// Set page size
732    pub fn psize(mut self, psize: i32) -> Self {
733        self.psize = Some(psize);
734        self
735    }
736}
737
738/// Playlist information
739#[derive(Debug, Deserialize, Serialize, Clone)]
740pub struct PlaylistInfo {
741    /// Playlist name
742    pub name: String,
743    /// Playlist ID
744    pub mb_id: u64,
745    /// File count in playlist
746    pub file_count: u32,
747    /// Creation time (timestamp)
748    pub ctime: u32,
749    /// Modification time (timestamp)
750    pub mtime: u32,
751    /// Broadcast type (0: audio)
752    pub btype: u32,
753    /// Broadcast sub-type (0: normal, 1: music, 2: course)
754    pub bstype: u32,
755}
756
757/// List of playlists
758#[derive(Debug, Deserialize, Serialize, Clone)]
759pub struct PlaylistList {
760    pub has_more: u32,
761    pub list: Vec<PlaylistInfo>,
762}
763
764/// Playlist file information
765#[derive(Debug, Deserialize, Serialize, Clone)]
766pub struct PlaylistFileInfo {
767    /// File server ID
768    pub fs_id: String,
769    /// File path
770    pub path: String,
771    /// Broadcast file modification time
772    pub broadcast_file_mtime: u64,
773    /// Server creation time (optional)
774    #[serde(default)]
775    pub server_ctime: Option<String>,
776    /// Server modification time (optional)
777    #[serde(default)]
778    pub server_mtime: Option<String>,
779    /// Local creation time (optional)
780    #[serde(default)]
781    pub local_ctime: Option<String>,
782    /// Local modification time (optional)
783    #[serde(default)]
784    pub local_mtime: Option<String>,
785    /// Is directory (optional)
786    #[serde(default)]
787    pub isdir: Option<String>,
788    /// File size (optional)
789    #[serde(default)]
790    pub size: Option<String>,
791    /// File category (optional)
792    #[serde(default)]
793    pub category: Option<String>,
794    /// Server MD5 hash (optional)
795    #[serde(default)]
796    pub md5: Option<String>,
797    /// Privacy level (optional)
798    #[serde(default)]
799    pub privacy: Option<String>,
800    /// File name (optional)
801    #[serde(default)]
802    pub server_filename: Option<String>,
803}
804
805/// List of playlist files
806#[derive(Debug, Deserialize, Serialize, Clone)]
807pub struct PlaylistFileList {
808    pub has_more: u32,
809    pub list: Vec<PlaylistFileInfo>,
810}
811
812/// Media file entry for playback
813#[derive(Debug, Deserialize, Serialize, Clone)]
814pub struct MediaFileEntry {
815    /// File ID
816    pub fs_id: u64,
817    /// File name
818    pub server_filename: String,
819    /// File path
820    pub path: String,
821    /// File size
822    pub size: u64,
823    /// Category
824    pub category: i32,
825    /// Media info
826    pub media_info: Option<MediaInfo>,
827}
828
829/// Media information
830#[derive(Debug, Deserialize, Serialize, Clone)]
831pub struct MediaInfo {
832    /// Video or audio streams
833    pub streams: Option<Vec<MediaStream>>,
834    /// Duration in seconds
835    pub duration: Option<f64>,
836    /// Bitrate
837    pub bitrate: Option<i32>,
838}
839
840/// Media stream information
841#[derive(Debug, Deserialize, Serialize, Clone)]
842pub struct MediaStream {
843    /// Stream type
844    pub stream_type: Option<String>,
845    /// Video width
846    pub width: Option<i32>,
847    /// Video height
848    pub height: Option<i32>,
849    /// Codec
850    pub codec: Option<String>,
851    /// Playback URL
852    pub url: Option<String>,
853    /// Video or audio file
854    pub file: Option<MediaFile>,
855}
856
857/// Media file for playback
858#[derive(Debug, Deserialize, Serialize, Clone)]
859pub struct MediaFile {
860    /// File size
861    pub size: u64,
862    /// MD5
863    pub md5: String,
864    /// Server filename
865    pub server_filename: String,
866    /// Path
867    pub path: String,
868    /// File extension
869    pub file_ext: String,
870    /// Video info
871    pub video: Option<VideoInfo>,
872    /// Audio info
873    pub audio: Option<AudioInfo>,
874}
875
876/// Video information
877#[derive(Debug, Deserialize, Serialize, Clone)]
878pub struct VideoInfo {
879    pub width: Option<i32>,
880    pub height: Option<i32>,
881    pub duration: Option<f64>,
882    pub bitrate: Option<i32>,
883    pub codec: Option<String>,
884}
885
886/// Audio information
887#[derive(Debug, Deserialize, Serialize, Clone)]
888pub struct AudioInfo {
889    pub duration: Option<f64>,
890    pub bitrate: Option<i32>,
891    pub codec: Option<String>,
892    pub sample_rate: Option<i32>,
893}
894
895/// Media playback information
896#[derive(Debug, Deserialize, Serialize, Clone)]
897pub struct MediaPlayInfo {
898    pub list: Vec<MediaFileEntry>,
899    pub request_id: u64,
900}
901
902#[derive(Debug, Deserialize)]
903struct PlaylistListResponse {
904    has_more: u32,
905    #[serde(default)]
906    list: Option<Vec<PlaylistInfo>>,
907    errno: i32,
908    errmsg: Option<String>,
909}
910
911#[derive(Debug, Deserialize)]
912struct PlaylistFileListResponse {
913    has_more: u32,
914    #[serde(default)]
915    list: Option<Vec<PlaylistFileInfo>>,
916    errno: i32,
917    errmsg: Option<String>,
918}
919
920#[derive(Debug, Deserialize)]
921struct MediaPlayInfoResponse {
922    #[serde(default)]
923    list: Option<Vec<MediaFileEntry>>,
924    request_id: u64,
925    errno: i32,
926    errmsg: Option<String>,
927}