uploaders
This commit is contained in:
216
src/uploaders.rs
Normal file
216
src/uploaders.rs
Normal file
@@ -0,0 +1,216 @@
|
||||
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"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user