Skip to content

Commit 2d55191

Browse files
committed
add language
1 parent 88b110a commit 2d55191

10 files changed

Lines changed: 207 additions & 61 deletions

Cargo.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@ categories = [
1313
"authentication",
1414
"web-programming::http-client",
1515
]
16-
exclude = ["client_secret.json", "youtube-oauth2.json", "example-config.yaml"]
16+
exclude = [
17+
"client_secret.json",
18+
"youtube-oauth2.json",
19+
"example-batch-config.yaml",
20+
"example-individual-config.yaml",
21+
]
1722

1823
[lib]
1924
name = "rust_yt_uploader"

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ A high-performance, memory-safe Rust library for YouTube video uploading with OA
55
## Features
66

77
- **OAuth 2.0 Authentication**: Secure authentication with YouTube API using OAuth 2.0 flow with PKCE support
8-
- **Dual Configuration Formats**: Support for both legacy and batch YAML configuration formats
8+
- **Dual Configuration Formats**: Support for both individual and batch YAML configuration formats
99
- **Concurrent Uploads**: Async upload mode with configurable concurrency (default: 3)
1010
- **Resumable Uploads**: Robust upload handling with automatic retry and resumption
1111
- **Progress Tracking**: Real-time upload progress bars for sequential uploads
@@ -135,7 +135,7 @@ files:
135135
- "/path/to/video2.mp4"
136136
```
137137
138-
#### Legacy Format
138+
#### Individual Format
139139
140140
```yaml
141141
videos:
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ common:
1919
# Get this from your YouTube playlist URL
2020
playlistId: "PL1234567890123456"
2121

22+
# Default audio language for the video (e.g., "en", "zh-Hans", "es")
23+
defaultAudioLanguage: "en"
24+
25+
# Default language for the video (e.g., "en", "zh-Hans", "es")
26+
defaultLanguage: "en"
27+
28+
# Recording date in ISO 8601 format (YYYY-MM-DD will be converted to YYYY-MM-DDT00:00:00.000Z)
29+
recordingDate: "2026-01-24"
30+
2231
# List of video titles (will be prefixed with common.prefix)
2332
titles:
2433
- "Episode 1: Introduction to Rust"

example-individual-config.yaml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Example individual configuration file for Rust YouTube Uploader
2+
# This demonstrates the individual configuration format with individual video configs
3+
4+
videos:
5+
- title: "My First Video"
6+
description: "This is my first video uploaded with Rust uploader"
7+
keywords: "rust,youtube,first,video"
8+
file: "/path/to/video1.mp4"
9+
category: 22 # People & Blogs
10+
privacyStatus: "private"
11+
playlistId: "PL1234567890123456"
12+
defaultAudioLanguage: "en"
13+
defaultLanguage: "en"
14+
recordingDate: "2026-01-24"
15+
16+
- title: "My Second Video"
17+
description: "This is my second video with different settings"
18+
keywords: "rust,youtube,second,tutorial"
19+
file: "/path/to/video2.mp4"
20+
category: 28 # Science & Technology
21+
privacyStatus: "unlisted"
22+
playlistId: "PL1234567890123456"
23+
defaultAudioLanguage: "en"
24+
defaultLanguage: "en"
25+
recordingDate: "2026-01-25"
26+
27+
- title: "My Third Video"
28+
description: "A public video for everyone to see"
29+
keywords: "rust,youtube,public,demo"
30+
file: "/path/to/video3.mp4"
31+
category: 28 # Science & Technology
32+
privacyStatus: "public"
33+
playlistId: "PL1234567890123456"
34+
defaultAudioLanguage: "en"
35+
defaultLanguage: "en"
36+
recordingDate: "2026-01-26"

example-legacy-config.yaml

Lines changed: 0 additions & 27 deletions
This file was deleted.

src/bin/yt_upload.rs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
77
use anyhow::Result;
88
use clap::Parser;
9-
use rust_yt_uploader::{BatchConfigRoot, ConfigFormat, LegacyConfigRoot};
9+
use rust_yt_uploader::{BatchConfigRoot, ConfigFormat, IndividualConfigRoot};
1010
use rust_yt_uploader::{
11-
upload_batch_concurrent, upload_batch_sequential, upload_legacy_sequential,
11+
upload_batch_concurrent, upload_batch_sequential, upload_individual_sequential,
1212
};
1313
use std::path::PathBuf;
1414
use tracing::info;
@@ -21,7 +21,7 @@ use tracing::info;
2121
Upload videos to YouTube from a YAML configuration file.
2222
2323
Supports two YAML schema formats:
24-
- Legacy: 'videos' array with per-video configuration
24+
- Individual: 'videos' array with per-video configuration
2525
- Batch: 'common' config + separate 'titles' and 'files' arrays
2626
2727
Async mode uploads multiple videos concurrently for better performance.
@@ -50,7 +50,7 @@ struct Cli {
5050
/// * `config` - Raw YAML configuration as a string
5151
///
5252
/// # Returns
53-
/// * `ConfigFormat` - Either Legacy or Batch
53+
/// * `ConfigFormat` - Either Individual or Batch
5454
///
5555
/// # Errors
5656
/// * Returns error if schema cannot be determined
@@ -59,7 +59,7 @@ fn detect_yaml_schema(config: &str) -> Result<ConfigFormat> {
5959

6060
if let Some(mapping) = value.as_mapping() {
6161
if mapping.contains_key("videos") {
62-
return Ok(ConfigFormat::Legacy);
62+
return Ok(ConfigFormat::Individual);
6363
}
6464

6565
if mapping.contains_key("common")
@@ -71,7 +71,7 @@ fn detect_yaml_schema(config: &str) -> Result<ConfigFormat> {
7171
}
7272

7373
anyhow::bail!(
74-
"Unable to determine YAML schema. Expected either 'videos' key (legacy) or 'common', 'titles', and 'files' keys (batch)."
74+
"Unable to determine YAML schema. Expected either 'videos' key (individual) or 'common', 'titles', and 'files' keys (batch)."
7575
);
7676
}
7777

@@ -104,12 +104,12 @@ async fn main() -> Result<()> {
104104
let schema_type = detect_yaml_schema(&config_content)?;
105105

106106
match schema_type {
107-
ConfigFormat::Legacy => {
108-
info!("Detected legacy YAML schema format");
109-
let config: LegacyConfigRoot = serde_yaml_ng::from_str(&config_content)
110-
.map_err(|e| anyhow::anyhow!("Failed to parse legacy config: {}", e))?;
107+
ConfigFormat::Individual => {
108+
info!("Detected individual YAML schema format");
109+
let config: IndividualConfigRoot = serde_yaml_ng::from_str(&config_content)
110+
.map_err(|e| anyhow::anyhow!("Failed to parse individual config: {}", e))?;
111111

112-
upload_legacy_sequential(config, cli.progress).await?;
112+
upload_individual_sequential(config, cli.progress).await?;
113113
}
114114
ConfigFormat::Batch => {
115115
info!("Detected batch YAML schema format");

src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ pub mod youtube_client;
1212
// Re-export commonly used types
1313
pub use google_oauth::{Credentials, GoogleOAuth};
1414
pub use models::{
15-
BatchConfigRoot, CommonConfig, ConfigFormat, LegacyConfigRoot, PrivacyStatus, RetryConfig,
15+
BatchConfigRoot, CommonConfig, ConfigFormat, IndividualConfigRoot, PrivacyStatus, RetryConfig,
1616
VideoCategory, VideoConfig, VideoUploadOptions,
1717
};
1818
pub use youtube_client::{
19-
YouTubeClient, upload_batch_concurrent, upload_batch_sequential, upload_legacy_sequential,
19+
YouTubeClient, upload_batch_concurrent, upload_batch_sequential, upload_individual_sequential,
2020
};
2121

2222
pub use retry::retry_with_backoff;

src/models.rs

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! Configuration models for YouTube uploader with validation.
22
//!
33
//! This module provides Serde-based models that mirror the Python Pydantic models,
4-
//! supporting both legacy and batch YAML configuration formats.
4+
//! supporting both individual and batch YAML configuration formats.
55
66
use anyhow::{Result, anyhow};
77
use futures::future::try_join_all;
@@ -14,7 +14,7 @@ use validator::{Validate, ValidationError};
1414
/// Configuration format detection result
1515
#[derive(Debug, Clone, PartialEq)]
1616
pub enum ConfigFormat {
17-
Legacy,
17+
Individual,
1818
Batch,
1919
}
2020

@@ -67,6 +67,25 @@ pub struct VideoUploadOptions {
6767
pub category: u32,
6868
pub privacy_status: String,
6969
pub playlist_id: String,
70+
#[serde(rename = "defaultAudioLanguage")]
71+
pub default_audio_language: String,
72+
#[serde(rename = "defaultLanguage")]
73+
pub default_language: String,
74+
#[serde(rename = "recordingDate")]
75+
pub recording_date: String,
76+
}
77+
78+
impl VideoUploadOptions {
79+
/// Convert recording_date from "YYYY-MM-DD" to YouTube API timestamp format "YYYY-MM-DDTHH:MM:SS.000Z"
80+
pub fn formatted_recording_date(&self) -> String {
81+
if self.recording_date.contains('T') {
82+
// Already in timestamp format, return as-is
83+
self.recording_date.clone()
84+
} else {
85+
// Convert "YYYY-MM-DD" to "YYYY-MM-DDT00:00:00.000Z"
86+
format!("{}T00:00:00.000Z", self.recording_date)
87+
}
88+
}
7089
}
7190

7291
/// Valid YouTube video privacy status values.
@@ -227,6 +246,18 @@ pub struct CommonConfig {
227246
#[validate(custom(function = "validate_playlist_id"))]
228247
#[serde(rename = "playlistId")]
229248
pub playlist_id: String,
249+
250+
/// Default audio language for the video
251+
#[serde(rename = "defaultAudioLanguage")]
252+
pub default_audio_language: String,
253+
254+
/// Default language for the video
255+
#[serde(rename = "defaultLanguage")]
256+
pub default_language: String,
257+
258+
/// Recording date for the video
259+
#[serde(rename = "recordingDate")]
260+
pub recording_date: String,
230261
}
231262

232263
impl CommonConfig {
@@ -239,7 +270,7 @@ impl CommonConfig {
239270
}
240271
}
241272

242-
/// Configuration for a single video (legacy format).
273+
/// Configuration for a single video (individual format).
243274
#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
244275
pub struct VideoConfig {
245276
/// Video title
@@ -269,11 +300,23 @@ pub struct VideoConfig {
269300
#[validate(custom(function = "validate_playlist_id"))]
270301
#[serde(rename = "playlistId")]
271302
pub playlist_id: String,
303+
304+
/// Default audio language for the video
305+
#[serde(rename = "defaultAudioLanguage")]
306+
pub default_audio_language: String,
307+
308+
/// Default language for the video
309+
#[serde(rename = "defaultLanguage")]
310+
pub default_language: String,
311+
312+
/// Recording date for the video
313+
#[serde(rename = "recordingDate")]
314+
pub recording_date: String,
272315
}
273316

274-
/// Root model for legacy YAML format with videos array.
317+
/// Root model for individual YAML format with videos array.
275318
#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
276-
pub struct LegacyConfigRoot {
319+
pub struct IndividualConfigRoot {
277320
/// Test mode flag - if true, delete videos after upload
278321
#[serde(default = "bool::default")]
279322
pub test: bool,
@@ -365,4 +408,36 @@ mod tests {
365408
assert!(validate_playlist_id("invalid").is_err());
366409
assert!(validate_playlist_id("PL123").is_err()); // too short
367410
}
411+
412+
#[test]
413+
fn test_formatted_recording_date() {
414+
let options = VideoUploadOptions {
415+
file: "test.mp4".to_string(),
416+
title: "Test".to_string(),
417+
description: "Test".to_string(),
418+
keywords: "test".to_string(),
419+
category: 22,
420+
privacy_status: "private".to_string(),
421+
playlist_id: "PL1234567890123456".to_string(),
422+
default_audio_language: "en".to_string(),
423+
default_language: "en".to_string(),
424+
recording_date: "2026-01-24".to_string(),
425+
};
426+
427+
// Should convert YYYY-MM-DD to YYYY-MM-DDT00:00:00.000Z
428+
assert_eq!(
429+
options.formatted_recording_date(),
430+
"2026-01-24T00:00:00.000Z"
431+
);
432+
433+
// Already in timestamp format should remain unchanged
434+
let options_with_timestamp = VideoUploadOptions {
435+
recording_date: "2026-01-24T12:30:45.000Z".to_string(),
436+
..options
437+
};
438+
assert_eq!(
439+
options_with_timestamp.formatted_recording_date(),
440+
"2026-01-24T12:30:45.000Z"
441+
);
442+
}
368443
}

0 commit comments

Comments
 (0)