Skip to content

Commit 288e0fc

Browse files
committed
feat: use profile-based client_secret.json naming
- Add credentials_path_for_profile() to generate client_secret-{profile}.json paths - Remove default_credentials_path() - profile is now required for all operations - Update YouTubeClient::new() and with_progress_reporter() to use profile-based credentials - Update all CLI binaries to document profile-based file naming in help text - Update README.md and AGENTS.md with new profile-based usage examples - Update integration tests to use credentials_path_for_profile() With this change, using -p/--profile work will: - Load credentials from client_secret-work.json - Save/load tokens from youtube-oauth2-work.json This allows users to manage multiple YouTube API credentials for different profiles/channels.
1 parent 6dafbc0 commit 288e0fc

19 files changed

Lines changed: 744 additions & 201 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@ Thumbs.db
2525

2626
# OAuth credentials and tokens (NEVER commit these)
2727
client_secret.json
28+
client_secret-*.json
2829
*-oauth2.json
2930
youtube-oauth2.json
31+
youtube-oauth2-*.json
3032

3133
# Test files
3234
test_videos/

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ Update `Cargo.toml` version before committing; pre-commit hook validates changes
130130

131131
- ❌ Never: `cargo clean` (breaks build cache)
132132
- ❌ Never: Run binaries from `target/` directly (use `cargo run --bin`)
133-
- ❌ Never: Commit `client_secret.json`, `youtube-oauth2.json`
133+
- ❌ Never: Commit `client_secret-{profile}.json`, `youtube-oauth2-{profile}.json`
134134
- ✅ Always: Use `cargo test` before committing
135135
- ✅ Always: Run `cargo fmt` on modified files
136136
- ✅ Always: Test both sequential and concurrent upload modes if modifying `YouTubeClient`

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,14 @@ default = []
6161
use-yup-oauth2 = ["yup-oauth2"]
6262

6363
[dependencies]
64-
# Async runtime - minimal features for faster compilation
64+
# Async runtime - optimized for I/O and process management
6565
tokio = { version = "1.50.0", features = [
6666
"macros",
6767
"rt-multi-thread",
6868
"time",
6969
"sync",
7070
"fs",
71+
"process",
7172
] }
7273

7374
# HTTP client - native-tls uses system OpenSSL, eliminating the 4.4s aws-lc-sys C build.

README.md

Lines changed: 43 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -30,21 +30,22 @@ rust-yt-uploader = "0.2.8"
3030
#### Prerequisites
3131

3232
- A Google Cloud project with YouTube Data API v3 enabled
33-
- OAuth 2.0 client credentials (`client_secret.json`)
33+
- OAuth 2.0 client credentials
3434

3535
1. Go to the [Google Cloud Console](https://console.cloud.google.com/)
3636
2. Create a new project or select an existing one
3737
3. Enable the YouTube Data API v3
3838
4. Create OAuth 2.0 credentials (Desktop application)
39-
5. Download the credentials as `client_secret.json`
40-
6. Place the file in the parent directory of the Rust project
39+
5. Download the credentials and save as `client_secret-{profile}.json` (e.g., `client_secret-work.json`)
40+
6. Place the file in the working directory
4141

42-
The first time you run the uploader, it will:
42+
The first time you run the uploader with a profile, it will:
4343

44-
1. Display an authorization URL
45-
2. Open your browser for authentication
46-
3. Ask you to paste the authorization code
47-
4. Save the tokens to `youtube-oauth2.json`
44+
1. Load credentials from `client_secret-{profile}.json`
45+
2. Display an authorization URL
46+
3. Open your browser for authentication
47+
4. Ask you to paste the authorization code
48+
5. Save the tokens to `youtube-oauth2-{profile}.json`
4849

4950
#### Build from Source
5051

@@ -70,11 +71,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
7071
// Load configuration
7172
let config = BatchConfigRoot::from_file("config.yaml")?;
7273

73-
// Create authenticated client
74-
let client = YouTubeClient::new(
75-
"client_secret.json",
76-
"youtube-oauth2.json"
77-
).await?;
74+
// Create authenticated client with profile
75+
// Credentials: client_secret-work.json
76+
// Token: youtube-oauth2-work.json
77+
let client = YouTubeClient::new("work").await?;
7878

7979
// Upload videos
8080
client.upload_batch(&config).await?;
@@ -89,14 +89,15 @@ For more advanced use cases requiring direct API access:
8989

9090
```rust
9191
use rust_yt_uploader::google_oauth::{GoogleOAuth, Credentials};
92-
use rust_yt_uploader::youtube_client;
92+
use rust_yt_uploader::{youtube_client, credentials_path_for_profile, token_path_for_profile};
9393

9494
#[tokio::main]
9595
async fn main() -> Result<(), Box<dyn std::error::Error>> {
96-
// Create OAuth client with custom scopes
96+
// Create OAuth client with profile-based paths
97+
let profile = "work";
9798
let oauth_client = GoogleOAuth::new(
98-
"client_secret.json",
99-
"youtube-oauth2.json",
99+
credentials_path_for_profile(profile)?, // client_secret-work.json
100+
token_path_for_profile(profile)?, // youtube-oauth2-work.json
100101
youtube_client::default_youtube_scopes(),
101102
youtube_client::build_youtube_base_url(),
102103
).await?;
@@ -190,17 +191,17 @@ Example: `PL1234567890123456`
190191
#### yt-upload: Upload Videos
191192

192193
```bash
193-
# Sequential upload (default)
194-
yt-upload --file config.yaml
194+
# Sequential upload with profile (required)
195+
yt-upload --file config.yaml --profile work
195196
196197
# Sequential upload with progress bars
197-
yt-upload --file config.yaml --progress
198+
yt-upload --file config.yaml --profile work --progress
198199
199200
# Concurrent upload (3 concurrent by default)
200-
yt-upload --file config.yaml --async
201+
yt-upload --file config.yaml --profile work --async
201202
202203
# Custom concurrency level
203-
yt-upload --file config.yaml --async --concurrent 5
204+
yt-upload --file config.yaml --profile work --async --concurrent 5
204205
```
205206

206207
#### yt-list: List and Export Videos
@@ -213,25 +214,20 @@ The `yt-list` tool lists all videos from your YouTube channel with comprehensive
213214
**Basic Usage:**
214215

215216
```bash
216-
# List all videos in table format (default)
217-
yt-list
217+
# List all videos in table format (default) - profile required
218+
yt-list --profile work
218219
219220
# Export as JSON (for programmatic access)
220-
yt-list --format json
221+
yt-list --profile work --format json
221222
222223
# Export as JSONL (one video per line, useful for piping)
223-
yt-list --format jsonl
224+
yt-list --profile work --format jsonl
224225
225226
# Save to file instead of stdout
226-
yt-list --format json --output videos.json
227+
yt-list --profile work --format json --output videos.json
227228
228229
# Show only video IDs (one per line)
229-
yt-list --ids-only
230-
231-
# Filter by privacy status
232-
yt-list --status private
233-
yt-list --status public
234-
yt-list --status unlisted
230+
yt-list --profile work --ids-only
235231
```
236232

237233
**Output Formats:**
@@ -258,14 +254,14 @@ Each video includes:
258254
**Examples:**
259255

260256
```bash
261-
# Export public videos to JSON and pipe to jq for further processing
262-
yt-list --format json --status public | jq '.[].id'
257+
# Export videos to JSON and pipe to jq for further processing
258+
yt-list --profile work --format json | jq '.[].id'
263259
264260
# Extract video IDs and titles for batch download
265-
yt-list --format jsonl | jq -r '[.id, .title] | join(": ")'
261+
yt-list --profile work --format jsonl | jq -r '[.id, .title] | join(": ")'
266262
267263
# Save all video metadata for backup
268-
yt-list --format json --output my_videos_backup.json
264+
yt-list --profile work --format json --output my_videos_backup.json
269265
```
270266

271267
#### yt-update-lang: Update Language Metadata
@@ -280,17 +276,17 @@ For any videos that don't already have these values set.
280276
**Basic Usage:**
281277

282278
```bash
283-
# Show what would be updated (dry run)
284-
yt-update-lang --dry-run
279+
# Show what would be updated (dry run) - profile required
280+
yt-update-lang --profile work --dry-run
285281
286282
# Update all public videos with language metadata
287-
yt-update-lang
283+
yt-update-lang --profile work
288284
289285
# Verbose mode - show each video being processed
290-
yt-update-lang --verbose
286+
yt-update-lang --profile work --verbose
291287
292288
# Only update videos with no language metadata at all
293-
yt-update-lang --only-empty
289+
yt-update-lang --profile work --only-empty
294290
```
295291

296292
**Features:**
@@ -305,16 +301,16 @@ yt-update-lang --only-empty
305301

306302
```bash
307303
# Preview changes before applying
308-
yt-update-lang --dry-run
304+
yt-update-lang --profile work --dry-run
309305
310306
# Update with verbose output to see what's happening
311-
yt-update-lang --verbose
307+
yt-update-lang --profile work --verbose
312308
313309
# Combine with yt-list to verify your videos first
314-
yt-list --status public --format json | jq '.[] | {id, title, default_language, default_audio_language}'
310+
yt-list --profile work --format json | jq '.[] | {id, title, default_language, default_audio_language}'
315311
316312
# Then update them
317-
yt-update-lang
313+
yt-update-lang --profile work
318314
```
319315

320316
**Use Cases:**
@@ -336,7 +332,7 @@ yt-update-lang
336332

337333
### Security Notes
338334

339-
- Never commit `client_secret.json` or token files to version control
335+
- Never commit `client_secret-{profile}.json` or `youtube-oauth2-{profile}.json` files to version control
340336
- Store credentials securely with appropriate file permissions (600)
341337
- Regularly rotate OAuth tokens if needed
342338
- Use private/unlisted privacy settings for sensitive content

src/bin/yt_add_pin_comment.rs

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
use anyhow::Result;
22
use clap::Parser;
3-
use rust_yt_uploader::{YouTubeClient, init_logging};
3+
use rust_yt_uploader::{YouTubeClient, init_logging, validate_profile_name};
44
use std::fs;
55
use std::path::Path;
6+
use tracing::info;
67

78
/// YouTube video comment poster CLI
89
#[derive(Parser)]
@@ -38,13 +39,22 @@ struct Cli {
3839
/// Force posting even if a pinned comment is detected (or detection fails)
3940
#[arg(long)]
4041
force: bool,
42+
43+
/// Profile name for OAuth (alphanumeric only)
44+
/// Credentials: client_secret-{profile}.json, Token: youtube-oauth2-{profile}.json
45+
#[arg(short, long, value_name = "PROFILE")]
46+
profile: String,
4147
}
4248

4349
#[tokio::main]
4450
async fn main() -> Result<()> {
4551
init_logging();
4652
let cli = Cli::parse();
4753

54+
// Validate profile name
55+
validate_profile_name(&cli.profile)?;
56+
info!("Using profile: {}", cli.profile);
57+
4858
let comment_path = Path::new(&cli.comment_file);
4959
if !comment_path.exists() {
5060
anyhow::bail!("Comment file not found: {}", cli.comment_file);
@@ -77,7 +87,7 @@ async fn main() -> Result<()> {
7787
println!("─────────────────────────────────────────");
7888
println!();
7989

80-
let client = YouTubeClient::new().await?;
90+
let client = YouTubeClient::new(&cli.profile).await?;
8191

8292
// Bail out early if the video already has a pinned comment and the flag is set
8393
if cli.skip_if_pinned {
@@ -130,8 +140,16 @@ async fn main() -> Result<()> {
130140
println!(" Comment ID: {}", comment_id);
131141

132142
if cli.pin {
133-
client.pin_comment(&comment_id).await?;
134-
println!("✓ Successfully pinned comment");
143+
match client.pin_comment(&comment_id).await {
144+
Ok(()) => {
145+
println!("✓ Successfully pinned comment");
146+
}
147+
Err(e) => {
148+
println!();
149+
println!("⚠ Failed to pin comment after retries: {}", e);
150+
println!("Comment was posted successfully; exiting gracefully...");
151+
}
152+
}
135153
}
136154

137155
Ok(())

src/bin/yt_add_tags.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
use anyhow::Result;
22
use clap::Parser;
3-
use rust_yt_uploader::{YouTubeClient, init_logging};
3+
use rust_yt_uploader::{YouTubeClient, init_logging, validate_profile_name};
44
use std::fs;
55
use std::path::Path;
6+
use tracing::info;
67

78
/// YouTube video tags updater CLI
89
#[derive(Parser)]
@@ -33,13 +34,22 @@ struct Cli {
3334

3435
/// Path to text file containing tags (separated by comma or semicolon)
3536
tags_file: String,
37+
38+
/// Profile name for OAuth (alphanumeric only)
39+
/// Credentials: client_secret-{profile}.json, Token: youtube-oauth2-{profile}.json
40+
#[arg(short, long, value_name = "PROFILE")]
41+
profile: String,
3642
}
3743

3844
#[tokio::main]
3945
async fn main() -> Result<()> {
4046
init_logging();
4147
let cli = Cli::parse();
4248

49+
// Validate profile name
50+
validate_profile_name(&cli.profile)?;
51+
info!("Using profile: {}", cli.profile);
52+
4353
let tags_path = Path::new(&cli.tags_file);
4454
if !tags_path.exists() {
4555
anyhow::bail!("Tags file not found: {}", cli.tags_file);
@@ -64,7 +74,7 @@ async fn main() -> Result<()> {
6474
println!("Updating video: {}", cli.video_id);
6575
println!();
6676

67-
let client = YouTubeClient::new().await?;
77+
let client = YouTubeClient::new(&cli.profile).await?;
6878

6979
client.update_video_tags(&cli.video_id, &tags).await?;
7080

src/bin/yt_append_description.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
use anyhow::Result;
22
use clap::Parser;
3-
use rust_yt_uploader::{YouTubeClient, init_logging};
3+
use rust_yt_uploader::{YouTubeClient, init_logging, validate_profile_name};
44
use std::fs;
55
use std::path::Path;
6+
use tracing::info;
67

78
/// YouTube video description appender CLI
89
#[derive(Parser)]
@@ -25,13 +26,22 @@ struct Cli {
2526

2627
/// Path to text file containing content to append
2728
content_file: String,
29+
30+
/// Profile name for OAuth (alphanumeric only)
31+
/// Credentials: client_secret-{profile}.json, Token: youtube-oauth2-{profile}.json
32+
#[arg(short, long, value_name = "PROFILE")]
33+
profile: String,
2834
}
2935

3036
#[tokio::main]
3137
async fn main() -> Result<()> {
3238
init_logging();
3339
let cli = Cli::parse();
3440

41+
// Validate profile name
42+
validate_profile_name(&cli.profile)?;
43+
info!("Using profile: {}", cli.profile);
44+
3545
let content_path = Path::new(&cli.content_file);
3646
if !content_path.exists() {
3747
anyhow::bail!("Content file not found: {}", cli.content_file);
@@ -61,7 +71,7 @@ async fn main() -> Result<()> {
6171
println!("─────────────────────────────────────────");
6272
println!();
6373

64-
let client = YouTubeClient::new().await?;
74+
let client = YouTubeClient::new(&cli.profile).await?;
6575

6676
// Check if content already exists in the description
6777
println!("Checking for duplicate content in video description...");

0 commit comments

Comments
 (0)