use log::{debug, info, error};
use serde::Deserialize;
use crate::auth::AccessToken;
use crate::errors::{NetDiskError, NetDiskResult};
use crate::http::HttpClient;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VideoQuality {
Quality480P,
Quality720P,
Quality1080P,
}
impl VideoQuality {
pub fn to_media_type(self) -> &'static str {
match self {
VideoQuality::Quality480P => "M3U8_AUTO_480",
VideoQuality::Quality720P => "M3U8_AUTO_720",
VideoQuality::Quality1080P => "M3U8_AUTO_1080",
}
}
pub fn highest_for_vip_level(vip_level: u32) -> Self {
match vip_level {
0 | 1 => VideoQuality::Quality480P,
_ => VideoQuality::Quality1080P,
}
}
pub fn available_for_vip_level(vip_level: u32) -> Vec<Self> {
match vip_level {
0 | 1 => vec![VideoQuality::Quality480P],
_ => vec![
VideoQuality::Quality480P,
VideoQuality::Quality720P,
VideoQuality::Quality1080P,
],
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AudioQuality {
Quality128K,
}
impl AudioQuality {
pub fn to_media_type(self) -> &'static str {
match self {
AudioQuality::Quality128K => "M3U8_MP3_128",
}
}
}
#[derive(Debug, Clone)]
pub struct PlaylistClient {
http_client: HttpClient,
app_id: Option<String>,
}
impl PlaylistClient {
pub fn new(http_client: HttpClient) -> Self {
PlaylistClient {
http_client,
app_id: None,
}
}
pub fn new_with_app_id(http_client: HttpClient, app_id: String) -> Self {
PlaylistClient {
http_client,
app_id: Some(app_id),
}
}
pub fn set_app_id(&mut self, app_id: String) {
self.app_id = Some(app_id);
}
pub async fn get_playlist_list(
&self,
access_token: &AccessToken,
) -> NetDiskResult<PlaylistList> {
self.get_playlist_list_with_options(access_token, PlaylistListOptions::default())
.await
}
pub async fn get_playlist_list_with_options(
&self,
access_token: &AccessToken,
options: PlaylistListOptions,
) -> NetDiskResult<PlaylistList> {
let mut params = Vec::new();
params.push(("method", "list".to_string()));
params.push(("access_token", access_token.access_token.clone()));
if let Some(p) = options.page {
params.push(("page", p.to_string()));
}
if let Some(s) = options.psize {
params.push(("psize", s.to_string()));
}
let params_ref: Vec<(&str, &str)> = params.iter().map(|(k, v)| (*k, v.as_str())).collect();
debug!("Getting playlist list with options: {:?}", options);
let response: PlaylistListResponse = self
.http_client
.get("/rest/2.0/xpan/broadcast/list", Some(¶ms_ref))
.await?;
if response.errno != 0 {
let errmsg = response.errmsg.as_deref().unwrap_or("Unknown error");
return Err(NetDiskError::api_error(response.errno, errmsg));
}
let list = response.list.unwrap_or_default();
info!(
"Playlist list retrieved successfully, count: {}",
list.len()
);
Ok(PlaylistList {
has_more: response.has_more,
list,
})
}
pub async fn get_playlist_file_list(
&self,
access_token: &AccessToken,
mb_id: u64,
) -> NetDiskResult<PlaylistFileList> {
self.get_playlist_file_list_with_options(
access_token,
mb_id,
PlaylistFileListOptions::default(),
)
.await
}
pub async fn get_playlist_file_list_with_options(
&self,
access_token: &AccessToken,
mb_id: u64,
options: PlaylistFileListOptions,
) -> NetDiskResult<PlaylistFileList> {
let mut params = Vec::new();
params.push(("access_token", access_token.access_token.clone()));
params.push(("mb_id", mb_id.to_string()));
if let Some(s) = options.showmeta {
params.push(("showmeta", s.to_string()));
}
if let Some(p) = options.page {
params.push(("page", p.to_string()));
}
if let Some(s) = options.psize {
params.push(("psize", s.to_string()));
}
let params_ref: Vec<(&str, &str)> = params.iter().map(|(k, v)| (*k, v.as_str())).collect();
debug!(
"Getting playlist file list for mb_id: {} with options: {:?}",
mb_id, options
);
let response: PlaylistFileListResponse = self
.http_client
.post("/rest/2.0/xpan/broadcast/filelist", Some(¶ms_ref))
.await?;
if response.errno != 0 {
let errmsg = response.errmsg.as_deref().unwrap_or("Unknown error");
return Err(NetDiskError::api_error(response.errno, errmsg));
}
let list = response.list.unwrap_or_default();
info!(
"Playlist file list retrieved successfully, count: {}",
list.len()
);
Ok(PlaylistFileList {
has_more: response.has_more,
list,
})
}
pub async fn get_media_play_info(
&self,
access_token: &AccessToken,
fsid: Option<u64>,
path: Option<&str>,
media_type: &str,
) -> NetDiskResult<MediaPlayInfo> {
let mut params = Vec::new();
params.push(("method", "streaming".to_string()));
params.push(("access_token", access_token.access_token.clone()));
params.push(("type", media_type.to_string()));
if let Some(f) = fsid {
params.push(("fid", f.to_string()));
}
if let Some(p) = path {
params.push(("path", p.to_string()));
}
if let Some(app_id) = &self.app_id {
params.push(("app_id", app_id.clone()));
}
let params_ref: Vec<(&str, &str)> = params.iter().map(|(k, v)| (*k, v.as_str())).collect();
let headers = [
("User-Agent", "xpanvideo;netdisk;iPhone13;ios-iphone;15.1;ts"),
("Host", "pan.baidu.com"),
("Accept", "*/*"),
("Accept-Language", "zh-CN,zh;q=0.9"),
];
debug!("Getting media play info with params: {:?}", params_ref);
let response: MediaPlayInfoResponse = self
.http_client
.get_with_headers("/rest/2.0/xpan/file", Some(¶ms_ref), Some(&headers))
.await?;
if response.errno != 0 {
let errmsg = response.errmsg.as_deref().unwrap_or("Unknown error");
return Err(NetDiskError::api_error(response.errno, errmsg));
}
let list = response.list.unwrap_or_default();
info!("Media play info retrieved successfully");
Ok(MediaPlayInfo {
list,
request_id: response.request_id,
})
}
pub async fn fetch_m3u8(
&self,
m3u8_url: &str,
) -> NetDiskResult<String> {
debug!("Fetching m3u8 content from: {}", m3u8_url);
let client = reqwest::Client::new();
let response = client
.get(m3u8_url)
.header("User-Agent", "xpanvideo;netdisk;iPhone13;ios-iphone;15.1;ts")
.header("Host", "pan.baidu.com")
.send()
.await?;
if !response.status().is_success() {
return Err(NetDiskError::Unknown {
message: format!("Failed to fetch m3u8: {}", response.status()),
});
}
let content = response.text().await?;
debug!("Successfully fetched m3u8 content, length: {}", content.len());
Ok(content)
}
pub async fn is_media_fully_transcoded(
&self,
m3u8_url: &str,
) -> NetDiskResult<bool> {
let content = self.fetch_m3u8(m3u8_url).await?;
Ok(content.contains("#EXT-X-ENDLIST"))
}
pub async fn get_media_m3u8_content(
&self,
access_token: &AccessToken,
path: &str,
media_type: &str,
) -> NetDiskResult<String> {
let mut params = vec![
("method", "streaming".to_string()),
("access_token", access_token.access_token.clone()),
("path", path.to_string()),
("type", media_type.to_string()),
];
if let Some(app_id) = &self.app_id {
params.push(("app_id", app_id.clone()));
}
let params_ref: Vec<(&str, &str)> = params.iter().map(|(k, v)| (*k, v.as_str())).collect();
debug!("Getting raw m3u8 content for path: {}, type: {}", path, media_type);
let mut url = reqwest::Url::parse("https://pan.baidu.com/rest/2.0/xpan/file")?;
{
let mut pairs = url.query_pairs_mut();
for (key, value) in ¶ms_ref {
pairs.append_pair(key, value);
}
}
debug!("Built URL for m3u8: {}", url);
let client = reqwest::Client::new();
let response = client
.get(url.clone())
.header("User-Agent", "xpanvideo;netdisk;iPhone13;ios-iphone;15.1;ts")
.header("Host", "pan.baidu.com")
.header("Accept", "*/*")
.header("Accept-Language", "zh-CN,zh;q=0.9")
.send()
.await?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
error!("Failed to get m3u8 content: {} - {}", status, body);
return Err(NetDiskError::http_error(status.as_u16(), url.as_ref()));
}
let content = response.text().await?;
debug!("Successfully fetched m3u8 content, length: {}", content.len());
Ok(content)
}
pub async fn get_video_m3u8(
&self,
access_token: &AccessToken,
path: &str,
quality: VideoQuality,
) -> NetDiskResult<String> {
self.get_media_m3u8_content(access_token, path, quality.to_media_type())
.await
}
pub async fn get_video_m3u8_highest(
&self,
access_token: &AccessToken,
path: &str,
vip_level: u32,
) -> NetDiskResult<String> {
let quality = VideoQuality::highest_for_vip_level(vip_level);
self.get_video_m3u8(access_token, path, quality).await
}
pub async fn get_audio_m3u8(
&self,
access_token: &AccessToken,
path: &str,
quality: AudioQuality,
) -> NetDiskResult<String> {
self.get_media_m3u8_content(access_token, path, quality.to_media_type())
.await
}
pub async fn get_audio_m3u8_default(
&self,
access_token: &AccessToken,
path: &str,
) -> NetDiskResult<String> {
self.get_audio_m3u8(access_token, path, AudioQuality::Quality128K)
.await
}
}
#[derive(Debug, Clone, Default)]
pub struct PlaylistListOptions {
pub page: Option<i32>,
pub psize: Option<i32>,
}
impl PlaylistListOptions {
pub fn new() -> Self {
Self::default()
}
pub fn page(mut self, page: i32) -> Self {
self.page = Some(page);
self
}
pub fn psize(mut self, psize: i32) -> Self {
self.psize = Some(psize);
self
}
}
#[derive(Debug, Clone, Default)]
pub struct PlaylistFileListOptions {
pub showmeta: Option<i32>,
pub page: Option<i32>,
pub psize: Option<i32>,
}
impl PlaylistFileListOptions {
pub fn new() -> Self {
Self::default()
}
pub fn showmeta(mut self, showmeta: i32) -> Self {
self.showmeta = Some(showmeta);
self
}
pub fn page(mut self, page: i32) -> Self {
self.page = Some(page);
self
}
pub fn psize(mut self, psize: i32) -> Self {
self.psize = Some(psize);
self
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct PlaylistInfo {
pub name: String,
pub mb_id: u64,
pub file_count: u32,
pub ctime: u32,
pub mtime: u32,
pub btype: u32,
pub bstype: u32,
}
#[derive(Debug, Clone)]
pub struct PlaylistList {
pub has_more: u32,
pub list: Vec<PlaylistInfo>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct PlaylistFileInfo {
pub fs_id: String,
pub path: String,
pub broadcast_file_mtime: u64,
#[serde(default)]
pub server_ctime: Option<String>,
#[serde(default)]
pub server_mtime: Option<String>,
#[serde(default)]
pub local_ctime: Option<String>,
#[serde(default)]
pub local_mtime: Option<String>,
#[serde(default)]
pub isdir: Option<String>,
#[serde(default)]
pub size: Option<String>,
#[serde(default)]
pub category: Option<String>,
#[serde(default)]
pub md5: Option<String>,
#[serde(default)]
pub privacy: Option<String>,
#[serde(default)]
pub server_filename: Option<String>,
}
#[derive(Debug, Clone)]
pub struct PlaylistFileList {
pub has_more: u32,
pub list: Vec<PlaylistFileInfo>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct MediaFileEntry {
pub fs_id: u64,
pub server_filename: String,
pub path: String,
pub size: u64,
pub category: i32,
pub media_info: Option<MediaInfo>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct MediaInfo {
pub streams: Option<Vec<MediaStream>>,
pub duration: Option<f64>,
pub bitrate: Option<i32>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct MediaStream {
pub stream_type: Option<String>,
pub width: Option<i32>,
pub height: Option<i32>,
pub codec: Option<String>,
pub url: Option<String>,
pub file: Option<MediaFile>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct MediaFile {
pub size: u64,
pub md5: String,
pub server_filename: String,
pub path: String,
pub file_ext: String,
pub video: Option<VideoInfo>,
pub audio: Option<AudioInfo>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct VideoInfo {
pub width: Option<i32>,
pub height: Option<i32>,
pub duration: Option<f64>,
pub bitrate: Option<i32>,
pub codec: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct AudioInfo {
pub duration: Option<f64>,
pub bitrate: Option<i32>,
pub codec: Option<String>,
pub sample_rate: Option<i32>,
}
#[derive(Debug, Clone)]
pub struct MediaPlayInfo {
pub list: Vec<MediaFileEntry>,
pub request_id: u64,
}
#[derive(Debug, Deserialize)]
struct PlaylistListResponse {
has_more: u32,
#[serde(default)]
list: Option<Vec<PlaylistInfo>>,
errno: i32,
errmsg: Option<String>,
}
#[derive(Debug, Deserialize)]
struct PlaylistFileListResponse {
has_more: u32,
#[serde(default)]
list: Option<Vec<PlaylistFileInfo>>,
errno: i32,
errmsg: Option<String>,
}
#[derive(Debug, Deserialize)]
struct MediaPlayInfoResponse {
#[serde(default)]
list: Option<Vec<MediaFileEntry>>,
request_id: u64,
errno: i32,
errmsg: Option<String>,
}