217 lines
6.4 KiB
Rust
217 lines
6.4 KiB
Rust
use chrono::{SecondsFormat, TimeZone, Utc};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::videos::VideoItem;
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
|
pub struct UploadersRequest {
|
|
#[serde(default, alias = "uploader_id")]
|
|
pub uploaderId: Option<String>,
|
|
#[serde(default, alias = "uploader_name")]
|
|
pub uploaderName: Option<String>,
|
|
#[serde(default, alias = "profile_content")]
|
|
pub profileContent: bool,
|
|
#[serde(default)]
|
|
pub query: Option<String>,
|
|
}
|
|
|
|
impl UploadersRequest {
|
|
pub fn normalized(self) -> Self {
|
|
Self {
|
|
uploaderId: normalize_optional_string(self.uploaderId),
|
|
uploaderName: normalize_optional_string(self.uploaderName),
|
|
profileContent: self.profileContent,
|
|
query: normalize_optional_string(self.query),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
|
pub struct UploaderProfile {
|
|
pub id: String,
|
|
pub name: String,
|
|
pub url: Option<String>,
|
|
pub channel: Option<String>,
|
|
pub verified: bool,
|
|
pub videoCount: u64,
|
|
pub totalViews: u64,
|
|
#[serde(default)]
|
|
pub channels: Option<Vec<UploaderChannelStat>>,
|
|
#[serde(default, alias = "profile_picture_url")]
|
|
pub avatar: Option<String>,
|
|
pub description: Option<String>,
|
|
pub bio: Option<String>,
|
|
#[serde(default)]
|
|
pub videos: Option<Vec<UploaderVideoRef>>,
|
|
#[serde(default)]
|
|
pub tapes: Option<Vec<serde_json::Value>>,
|
|
#[serde(default)]
|
|
pub playlists: Option<Vec<serde_json::Value>>,
|
|
#[serde(default)]
|
|
pub layout: Option<Vec<UploaderLayoutRow>>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
|
pub struct UploaderChannelStat {
|
|
pub channel: String,
|
|
pub videoCount: u64,
|
|
#[serde(default, alias = "first_seen_at")]
|
|
pub firstSeenAt: Option<String>,
|
|
#[serde(default, alias = "last_seen_at")]
|
|
pub lastSeenAt: Option<String>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
|
pub struct UploaderVideoRef {
|
|
pub id: String,
|
|
pub url: String,
|
|
pub title: String,
|
|
pub duration: u32,
|
|
pub channel: String,
|
|
#[serde(default, alias = "uploaded_at")]
|
|
pub uploadedAt: Option<String>,
|
|
pub uploader: String,
|
|
#[serde(alias = "uploader_id")]
|
|
pub uploaderId: String,
|
|
pub thumb: String,
|
|
pub preview: Option<String>,
|
|
pub views: u32,
|
|
pub rating: u32,
|
|
#[serde(default, alias = "aspect_ratio")]
|
|
pub aspectRatio: Option<f32>,
|
|
}
|
|
|
|
impl UploaderVideoRef {
|
|
pub fn from_video_item(item: &VideoItem, uploader_name: &str, uploader_id: &str) -> Self {
|
|
Self {
|
|
id: item.id.clone(),
|
|
url: item.url.clone(),
|
|
title: item.title.clone(),
|
|
duration: item.duration,
|
|
channel: item.channel.clone(),
|
|
uploadedAt: iso_timestamp_from_unix(item.uploadedAt),
|
|
uploader: item
|
|
.uploader
|
|
.clone()
|
|
.unwrap_or_else(|| uploader_name.to_string()),
|
|
uploaderId: item
|
|
.uploaderId
|
|
.clone()
|
|
.unwrap_or_else(|| uploader_id.to_string()),
|
|
thumb: item.thumb.clone(),
|
|
preview: item.preview.clone(),
|
|
views: item.views.unwrap_or_default(),
|
|
rating: item.rating.map(normalize_rating).unwrap_or_default(),
|
|
aspectRatio: item.aspectRatio,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
|
pub struct UploaderLayoutRow {
|
|
#[serde(rename = "type")]
|
|
pub rowType: UploaderLayoutRowType,
|
|
pub title: Option<String>,
|
|
#[serde(default, alias = "video_ids")]
|
|
pub videoIds: Option<Vec<String>>,
|
|
}
|
|
|
|
impl UploaderLayoutRow {
|
|
pub fn horizontal(title: Option<String>, video_ids: Vec<String>) -> Self {
|
|
Self {
|
|
rowType: UploaderLayoutRowType::Horizontal,
|
|
title,
|
|
videoIds: Some(video_ids),
|
|
}
|
|
}
|
|
|
|
pub fn videos(title: Option<String>) -> Self {
|
|
Self {
|
|
rowType: UploaderLayoutRowType::Videos,
|
|
title,
|
|
videoIds: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq)]
|
|
pub enum UploaderLayoutRowType {
|
|
#[default]
|
|
#[serde(rename = "videos")]
|
|
Videos,
|
|
#[serde(rename = "horizontal", alias = "horizontal_videos")]
|
|
Horizontal,
|
|
}
|
|
|
|
pub fn normalize_optional_string(value: Option<String>) -> Option<String> {
|
|
value.and_then(|value| {
|
|
let trimmed = value.trim();
|
|
(!trimmed.is_empty()).then(|| trimmed.to_string())
|
|
})
|
|
}
|
|
|
|
pub fn iso_timestamp_from_unix(value: Option<u64>) -> Option<String> {
|
|
let timestamp = value?;
|
|
let dt = Utc.timestamp_opt(timestamp as i64, 0).single()?;
|
|
Some(dt.to_rfc3339_opts(SecondsFormat::Millis, true))
|
|
}
|
|
|
|
fn normalize_rating(value: f32) -> u32 {
|
|
value.clamp(0.0, 100.0).round() as u32
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn request_accepts_snake_case_aliases() {
|
|
let request: UploadersRequest = serde_json::from_str(
|
|
r#"{
|
|
"uploader_id": "hsex:xihongshiddd",
|
|
"uploader_name": "xihongshiddd",
|
|
"profile_content": true,
|
|
"query": "teacher"
|
|
}"#,
|
|
)
|
|
.expect("request should decode");
|
|
|
|
assert_eq!(request.uploaderId.as_deref(), Some("hsex:xihongshiddd"));
|
|
assert_eq!(request.uploaderName.as_deref(), Some("xihongshiddd"));
|
|
assert!(request.profileContent);
|
|
assert_eq!(request.query.as_deref(), Some("teacher"));
|
|
}
|
|
|
|
#[test]
|
|
fn layout_aliases_decode() {
|
|
let row: UploaderLayoutRow = serde_json::from_str(
|
|
r#"{
|
|
"type": "horizontal_videos",
|
|
"title": "For You",
|
|
"video_ids": ["one", "two"]
|
|
}"#,
|
|
)
|
|
.expect("row should decode");
|
|
|
|
assert_eq!(row.rowType, UploaderLayoutRowType::Horizontal);
|
|
assert_eq!(row.videoIds.as_ref().map(Vec::len), Some(2));
|
|
}
|
|
|
|
#[test]
|
|
fn avatar_alias_decodes() {
|
|
let profile: UploaderProfile = serde_json::from_str(
|
|
r#"{
|
|
"id": "abc",
|
|
"name": "Example",
|
|
"verified": false,
|
|
"videoCount": 1,
|
|
"totalViews": 2,
|
|
"profile_picture_url": "https://example.com/a.jpg"
|
|
}"#,
|
|
)
|
|
.expect("profile should decode");
|
|
|
|
assert_eq!(profile.avatar.as_deref(), Some("https://example.com/a.jpg"));
|
|
}
|
|
}
|