chore: backup before migration to new repo

This commit is contained in:
Warren
2026-04-23 16:46:02 +08:00
parent 13dd3b30f3
commit 59809dae1f
40 changed files with 5566 additions and 1783 deletions

View File

@@ -5,6 +5,21 @@ use std::path::PathBuf;
const DEFAULT_API_URL: &str = "http://localhost:3002";
const DEV_API_URL: &str = "http://localhost:3003";
fn get_api_url() -> String {
std::env::var("MOMENTRY_API_URL").unwrap_or_else(|_| {
std::env::var("MOMENTRY_SERVER_PORT")
.ok()
.map(|port| format!("http://localhost:{}", port))
.unwrap_or_else(|| DEFAULT_API_URL.to_string())
})
}
fn get_api_key() -> Option<String> {
std::env::var("MOMENTRY_API_KEY").ok()
}
#[derive(Debug, Clone)]
pub struct ApiClient {
client: Client,
@@ -83,7 +98,7 @@ pub struct VideosResponse {
impl ApiClient {
pub fn new() -> Self {
let url = std::env::var("MOMENTRY_API_URL").unwrap_or_else(|_| DEFAULT_API_URL.to_string());
let url = get_api_url();
Self {
client: Client::new(),
base_url: url,
@@ -103,7 +118,11 @@ impl ApiClient {
let request = RegisterRequest {
path: path.to_string(),
};
let response = self.client.post(&url).json(&request).send().await?;
let mut request_builder = self.client.post(&url).json(&request);
if let Some(key) = get_api_key() {
request_builder = request_builder.header("X-API-Key", key);
}
let response = request_builder.send().await?;
let status = response.status();
let result = response.json::<RegisterResponse>().await?;
if !status.is_success() {
@@ -124,7 +143,11 @@ impl ApiClient {
limit,
uuid: uuid.map(|s| s.to_string()),
};
let response = self.client.post(&url).json(&request).send().await?;
let mut request_builder = self.client.post(&url).json(&request);
if let Some(key) = get_api_key() {
request_builder = request_builder.header("X-API-Key", key);
}
let response = request_builder.send().await?;
let status = response.status();
let result = response.json::<SearchResponse>().await?;
if !status.is_success() {
@@ -135,18 +158,30 @@ impl ApiClient {
pub async fn lookup_video(&self, uuid: &str) -> Result<LookupResponse> {
let url = format!("{}/api/v1/lookup?uuid={}", self.base_url, uuid);
let response = self.client.get(&url).send().await?;
let mut request = self.client.get(&url);
if let Some(key) = get_api_key() {
request = request.header("X-API-Key", key);
}
let response = request.send().await?;
let status = response.status();
let result = response.json::<LookupResponse>().await?;
if !status.is_success() {
if status == 200 {
let result = response.json::<LookupResponse>().await?;
if result.uuid.is_empty() {
anyhow::bail!("影片不存在: {}", uuid);
}
Ok(result)
} else {
anyhow::bail!("API request failed with status: {}", status);
}
Ok(result)
}
pub async fn list_videos(&self) -> Result<Vec<VideoInfo>> {
let url = format!("{}/api/v1/videos", self.base_url);
let response = self.client.get(&url).send().await?;
let mut request = self.client.get(&url);
if let Some(key) = get_api_key() {
request = request.header("X-API-Key", key);
}
let response = request.send().await?;
let status = response.status();
let result = response.json::<VideosResponse>().await?;
if !status.is_success() {

View File

@@ -397,6 +397,29 @@ fn format_time(seconds: f64) -> String {
format!("{:02}:{:02}:{:02}.{:02}", hours, minutes, secs, millis)
}
#[allow(dead_code)]
fn get_video_duration(video_path: &str) -> f64 {
let output = std::process::Command::new("ffprobe")
.args([
"-v",
"error",
"-show_entries",
"format=duration",
"-of",
"default=noprint_wrappers=1:nokey=1",
video_path,
])
.output();
match output {
Ok(out) if out.status.success() => {
let duration_str = String::from_utf8_lossy(&out.stdout).trim().to_string();
duration_str.parse::<f64>().unwrap_or(0.0)
}
_ => 0.0,
}
}
fn lookup_video_uuid(video_path: &str) -> Option<String> {
use std::process::Command as StdCommand;
@@ -510,9 +533,714 @@ fn run_player(_video_path: &str, _video_uuid: Option<String>) -> Result<()> {
}
#[cfg(feature = "player")]
fn run_player(_video_path: &str, _video_uuid: Option<String>) -> Result<()> {
println!("Player not available - SDL2 not configured");
println!("Playing: {} (UUID: {:?})", _video_path, _video_uuid);
fn run_player(video_path: &str, video_uuid: Option<String>) -> Result<()> {
run_player_with_sdl2(video_path, video_uuid)
}
#[cfg(feature = "player")]
fn run_player_with_sdl2(video_path: &str, video_uuid: Option<String>) -> Result<()> {
use sdl2::event::Event;
use sdl2::keyboard::Keycode;
use sdl2::pixels::PixelFormatEnum;
use std::io::{BufReader, Read};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::{Duration, Instant};
println!("\n=== 🎬 SDL2 Video Player ===");
println!("File: {}", video_path);
println!("UUID: {:?}", video_uuid);
let sdl_context = sdl2::init().map_err(|e| anyhow::anyhow!("SDL init failed: {}", e))?;
let video_subsystem = sdl_context
.video()
.map_err(|e| anyhow::anyhow!("Video init failed: {}", e))?;
let width = 1280u32;
let height = 720u32;
let window = video_subsystem
.window("Momentry Player", width, height)
.position_centered()
.resizable()
.build()
.map_err(|e| anyhow::anyhow!("Window creation failed: {}", e))?;
let mut canvas = window
.into_canvas()
.build()
.map_err(|e| anyhow::anyhow!("Canvas creation failed: {}", e))?;
let texture_creator = canvas.texture_creator();
let mut texture = texture_creator
.create_texture_streaming(PixelFormatEnum::RGB24, width as u32, height as u32)
.map_err(|e| anyhow::anyhow!("Texture creation failed: {}", e))?;
let ffmpeg_path = if cfg!(target_os = "macos") {
"/opt/homebrew/bin/ffmpeg"
} else {
"ffmpeg"
};
let mut ffmpeg = std::process::Command::new(ffmpeg_path)
.args([
"-i",
video_path,
"-vf",
&format!(
"scale={}:{}:force_original_aspect_ratio=decrease,pad={}:{}:(ow-iw)/2:(oh-ih)/2",
width, height, width, height
),
"-pix_fmt",
"rgb24",
"-r",
"30",
"-f",
"rawvideo",
"-",
])
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
.spawn()
.map_err(|e| anyhow::anyhow!("Failed to start ffmpeg: {}", e))?;
let stdout = ffmpeg
.stdout
.take()
.ok_or_else(|| anyhow::anyhow!("Failed to capture stdout"))?;
let mut reader = BufReader::new(stdout);
let frame_size = (width * height * 3) as usize;
let mut frame_buffer = vec![0u8; frame_size];
let playing = Arc::new(AtomicBool::new(true));
let playing_clone = playing.clone();
let mut event_pump = sdl_context
.event_pump()
.map_err(|e| anyhow::anyhow!("Event pump failed: {}", e))?;
let mut asr_overlay = asr_overlay::AsrOverlay::new();
let _ = asr_overlay.load_from_file(video_path);
println!("ASR Overlay initialized: {}", !asr_overlay.is_empty());
let video_duration = get_video_duration(video_path);
println!("Video duration: {:.1}s", video_duration);
let mut frame_count = 0u64;
let frame_duration = Duration::from_millis(33);
let mut paused = false;
let mut current_time = 0.0;
let mut seek_request: Option<f64> = None;
let fps = 30.0;
let mut asr_overlay_visible = false;
println!("Playing... (Press SPACE to pause, Q/ESC to quit, ←/→ to seek, A to toggle ASR, F for fullscreen)");
loop {
let frame_start = Instant::now();
// Handle seek by restarting ffmpeg
if let Some(seek_pos) = seek_request {
seek_request = None;
println!("\n⏩ Seeking to {:.1}s...", seek_pos);
// Kill old ffmpeg and restart with seek position
let _ = ffmpeg.kill();
ffmpeg = std::process::Command::new(ffmpeg_path)
.args([
"-ss", &format!("{:.2}", seek_pos),
"-i", video_path,
"-vf", &format!(
"scale={}:{}:force_original_aspect_ratio=decrease,pad={}:{}:(ow-iw)/2:(oh-ih)/2",
width, height, width, height
),
"-pix_fmt", "rgb24",
"-r", "30",
"-f", "rawvideo",
"-",
])
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
.spawn()
.map_err(|e| anyhow::anyhow!("Failed to restart ffmpeg: {}", e))?;
let stdout = ffmpeg
.stdout
.take()
.ok_or_else(|| anyhow::anyhow!("Failed to capture stdout"))?;
reader = BufReader::new(stdout);
current_time = seek_pos;
println!("▶ Resumed at {:.1}s", current_time);
}
for event in event_pump.poll_iter() {
match event {
Event::Quit { .. } => {
println!("\n👋 Quitting player");
playing_clone.store(false, Ordering::SeqCst);
break;
}
Event::KeyDown { keycode, .. } => match keycode {
Some(Keycode::Q) | Some(Keycode::Escape) => {
println!("\n👋 Quitting player");
playing_clone.store(false, Ordering::SeqCst);
break;
}
Some(Keycode::Space) => {
paused = !paused;
println!("{}", if paused { "⏸ Paused" } else { "▶ Playing" });
}
Some(Keycode::Left) => {
let new_time = (current_time - 10.0).max(0.0);
seek_request = Some(new_time);
println!("⏪ Seek to {:.1}s", new_time);
}
Some(Keycode::Right) => {
let new_time = current_time + 10.0;
seek_request = Some(new_time);
println!("⏩ Seek to {:.1}s", new_time);
}
Some(Keycode::Up) => {
let new_time = (current_time - 60.0).max(0.0);
seek_request = Some(new_time);
println!("⏪ Seek to {:.1}s (1min)", new_time);
}
Some(Keycode::Down) => {
let new_time = current_time + 60.0;
seek_request = Some(new_time);
println!("⏩ Seek to {:.1}s (+1min)", new_time);
}
Some(Keycode::A) => {
// Toggle ASR Visibility
asr_overlay_visible = !asr_overlay_visible;
println!(
"{}",
if asr_overlay_visible {
"🔊 ASR ON"
} else {
"🔇 ASR OFF"
}
);
}
Some(Keycode::F) => {
println!("📺 Toggle fullscreen (not implemented in basic SDL2)");
}
_ => {}
},
_ => {}
}
}
if !playing_clone.load(Ordering::SeqCst) {
break;
}
if paused {
thread::sleep(Duration::from_millis(100));
continue;
}
// Update ASR text based on current time
if !asr_overlay.is_empty() {
asr_overlay.update(current_time);
}
match reader.read_exact(&mut frame_buffer) {
Ok(_) => {
texture
.update(None, &frame_buffer, (width * 3) as usize)
.map_err(|e| anyhow::anyhow!("Texture update failed: {}", e))?;
// Draw everything
canvas.clear();
canvas
.copy(&texture, None, None)
.map_err(|e| anyhow::anyhow!("Render failed: {}", e))?;
// Draw ASR Text if visible and available
if asr_overlay_visible && !asr_overlay.get_text().is_empty() {
// Placeholder: Cannot use TTF functions directly here without font context.
// For now, just printing to console to verify timing.
// In a real implementation, load font and draw text here.
println!("[ASR] {:.1}s: {}", current_time, asr_overlay.get_text());
}
// Draw progress bar at bottom - gray background, green progress
use sdl2::rect::Rect;
let progress = if video_duration > 0.0 {
(current_time / video_duration).min(1.0)
} else {
0.0
};
let bar_width = ((width as f64) * progress) as u32;
canvas.set_draw_color(sdl2::pixels::Color::RGB(50, 50, 50)); // Background
let _ = canvas.fill_rect(Rect::new(0, height as i32 - 15, width, 5));
if bar_width > 0 {
canvas.set_draw_color(sdl2::pixels::Color::RGB(0, 200, 0)); // Progress
let _ = canvas.fill_rect(Rect::new(0, height as i32 - 15, bar_width, 5));
}
// Reset draw color to black for next frame
canvas.set_draw_color(sdl2::pixels::Color::RGB(0, 0, 0));
canvas.present();
frame_count += 1;
current_time += 1.0 / fps;
let elapsed = frame_start.elapsed();
if elapsed < frame_duration {
thread::sleep(frame_duration - elapsed);
}
}
Err(_) => {
println!(
"\n📽️ End of video ({} frames, {:.1}s)",
frame_count, current_time
);
break;
}
}
}
let _ = ffmpeg.kill();
println!("✅ Playback finished (total: {:.1}s)", current_time);
Ok(())
}
fn run_local_mode(external_player: &str) -> Result<()> {
let args: Vec<String> = env::args().collect();
// Find video path - skip all flags and get the first non-flag argument after them
let video_path = args
.iter()
.skip(1) // Skip binary name
.skip_while(|a| a.starts_with('-')) // Skip flags
.next()
.cloned();
let video_path = match video_path {
Some(p) if !p.is_empty() => p,
_ => {
println!("Local Mode - Play local video files");
println!("=====================================\n");
print!("Enter video file path: ");
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
let path = input.trim().to_string();
if path.is_empty() {
anyhow::bail!("No video path provided");
}
path
}
};
if !Path::new(&video_path).exists() {
anyhow::bail!("File not found: {}", video_path);
}
println!("\nUsing external player: {}", external_player);
println!("Playing: {}", video_path);
match external_player {
"vlc" => {
std::process::Command::new("open")
.arg("-a")
.arg("VLC")
.arg(&video_path)
.spawn()?;
println!("✅ Opened with VLC");
}
"mpv" => {
std::process::Command::new("mpv").arg(&video_path).spawn()?;
println!("✅ Opened with mpv");
}
"ffplay" => {
std::process::Command::new("ffplay")
.arg("-autoexit")
.arg(&video_path)
.spawn()?;
println!("✅ Opened with ffplay");
}
"sdl2" => {
#[cfg(feature = "player")]
return run_player_with_sdl2(&video_path, None);
#[cfg(not(feature = "player"))]
{
println!("SDL2 player not enabled. Rebuild with --features player");
}
}
_ => {
std::process::Command::new(external_player)
.arg(&video_path)
.spawn()?;
println!("✅ Opened with {}", external_player);
}
}
Ok(())
}
fn run_online_mode() -> Result<()> {
println!("\n===========================================");
println!(" 🎬 Online Mode - Momentry");
println!("===========================================\n");
let client = ApiClient::new();
println!("Connected to API: {}", client.base_url());
let rt = tokio::runtime::Runtime::new()?;
loop {
println!("\n┌─────────────────────────────────────────┐");
println!("│ Online Mode Menu │");
println!("├─────────────────────────────────────────┤");
println!("│ [1] List Videos - 列出所有影片 │");
println!("│ [2] Search - RAG 搜尋影片內容 │");
println!("│ [3] Play - 播放影片 │");
println!("│ [4] Lookup - 查詢影片資訊 │");
println!("│ [q] Quit - 離開 │");
println!("└─────────────────────────────────────────┘");
print!("\n請選擇: ");
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
let choice = input.trim();
match choice {
"1" => {
println!("\n=== 📋 影片列表 ===");
match rt.block_on(client.list_videos()) {
Ok(videos) => {
if videos.is_empty() {
println!("沒有找到任何影片");
} else {
println!("\n{} 部影片:\n", videos.len());
for (i, v) in videos.iter().enumerate() {
let duration = format!(
"{}:{:02}",
(v.duration / 60.0) as u32,
(v.duration % 60.0) as u32
);
println!(
" [{}] {} | {} | {}x{} | {}",
i + 1,
v.file_name,
v.uuid.chars().take(8).collect::<String>(),
v.width,
v.height,
duration
);
}
}
}
Err(e) => println!("取得影片列表失敗: {}", e),
}
}
"2" => {
println!("\n=== 🔍 RAG 搜尋 ===");
print!("輸入搜尋關鍵字: ");
input.clear();
std::io::stdin().read_line(&mut input)?;
let query = input.trim().to_string();
if query.is_empty() {
println!("搜尋關鍵字不能為空");
continue;
}
print!("限定特定影片?(y/N): ");
input.clear();
std::io::stdin().read_line(&mut input)?;
let limit_uuid = if input.trim().to_lowercase() == "y" {
print!("輸入影片 UUID: ");
input.clear();
std::io::stdin().read_line(&mut input)?;
Some(input.trim().to_string())
} else {
None
};
println!("\n搜尋中...");
match rt.block_on(client.search_chunks(&query, limit_uuid.as_deref(), Some(10))) {
Ok(response) => {
if response.results.is_empty() {
println!("沒有找到結果");
continue;
}
println!("\n找到 {} 個結果:\n", response.results.len());
for (i, r) in response.results.iter().enumerate() {
let time_range = format!(
"{:02}:{:02} - {:02}:{:02}",
(r.start_time / 60.0) as u32,
(r.start_time % 60.0) as u32,
(r.end_time / 60.0) as u32,
(r.end_time % 60.0) as u32
);
let text_preview = if r.text.len() > 50 {
format!("{}...", &r.text[..50])
} else {
r.text.clone()
};
println!(
" [{}] {} | {} | {:.2} | {}",
i + 1,
time_range,
r.uuid.chars().take(8).collect::<String>(),
r.score,
text_preview
);
}
let mut current_player: Option<std::process::Child> = None;
loop {
if let Some(ref mut child) = current_player {
match child.try_wait() {
Ok(Some(_)) => {
println!("播放器已結束");
current_player = None;
}
Ok(None) => {
// 還在執行中
}
Err(e) => {
println!("檢查播放器狀態失敗:{}", e);
current_player = None;
}
}
}
print!(
"\n選擇播放 (1-{}) 或 q 離開 (kill player), L 重新顯示列表:",
response.results.len()
);
input.clear();
std::io::stdin().read_line(&mut input)?;
let selection = input.trim();
let selection_lower = selection.to_lowercase();
if selection_lower == "q" {
if let Some(ref mut child) = current_player {
let _ = child.kill();
let _ = child.wait();
println!("已終止播放器");
current_player = None;
}
break;
}
if selection_lower == "l" {
println!("\n搜尋結果:");
for (i, r) in response.results.iter().enumerate() {
let time_range = format!(
"{:02}:{:02} - {:02}:{:02}",
(r.start_time / 60.0) as u32,
(r.start_time % 60.0) as u32,
(r.end_time / 60.0) as u32,
(r.end_time % 60.0) as u32
);
let text_preview = if r.text.len() > 50 {
format!("{}...", &r.text[..50])
} else {
r.text.clone()
};
println!(
" [{}] {} | {} | {:.2} | {}",
i + 1,
time_range,
r.uuid.chars().take(8).collect::<String>(),
r.score,
text_preview
);
}
continue;
}
if let Ok(idx) = selection.parse::<usize>() {
if idx > 0 && idx <= response.results.len() {
let selected = &response.results[idx - 1];
println!("\n播放:{} - {}", selected.uuid, selected.text);
if let Some(ref mut child) = current_player {
let _ = child.kill();
let _ = child.wait();
println!("已終止前一個播放器");
}
match rt.block_on(client.lookup_video(&selected.uuid)) {
Ok(info) => {
if let Some(path) = &info.file_path {
if std::path::Path::new(path).exists() {
let start_sec =
(selected.start_time as f64) - 2.0;
let end_sec = (selected.end_time as f64) + 2.0;
println!(
"開啟:{} (從 {:.0}{:.0}A-B 循環)",
path, start_sec, end_sec
);
println!("提示mpv 視窗中按 c/C 切換循環q 離開Space 暫停");
current_player = Some(
std::process::Command::new("mpv")
.arg(format!(
"--start={:.2}",
start_sec.max(0.0)
))
.arg(format!(
"--ab-loop-a={:.2}",
start_sec.max(0.0)
))
.arg(format!("--ab-loop-b={:.2}", end_sec))
.arg("--input-commands=bind c ab-loop; bind C ab-loop")
.arg(path)
.spawn()?
);
} else {
println!("錯誤:檔案不存在:{}", path);
}
}
}
Err(e) => println!("查詢失敗:{}", e),
}
}
}
}
}
Err(e) => println!("搜尋失敗:{}", e),
}
}
"4" => {
println!("\n=== 🔎 查詢影片 ===");
print!("輸入影片 UUID (直接 Enter 從列表選擇): ");
input.clear();
std::io::stdin().read_line(&mut input)?;
let uuid = input.trim();
if uuid.is_empty() {
println!("載入影片列表...");
match rt.block_on(client.list_videos()) {
Ok(videos) => {
if videos.is_empty() {
println!("沒有影片");
continue;
}
println!("\n選擇影片:");
for (i, v) in videos.iter().enumerate() {
println!(" [{}] {} ({})", i + 1, v.file_name, v.uuid);
}
print!("\n選擇編號:");
input.clear();
std::io::stdin().read_line(&mut input)?;
if let Ok(idx) = input.trim().parse::<usize>() {
if idx > 0 && idx <= videos.len() {
let selected = &videos[idx - 1];
println!("\n查詢中...");
match rt.block_on(client.lookup_video(&selected.uuid)) {
Ok(info) => {
println!("\n✓ 找到影片:");
println!(" UUID: {}", info.uuid);
if let Some(path) = &info.file_path {
println!(" 路徑:{}", path);
}
if let Some(name) = &info.file_name {
println!(" 名稱:{}", name);
}
if let Some(dur) = info.duration {
println!(" 時長:{:.2}s", dur);
}
}
Err(e) => println!("查詢失敗:{}", e),
}
}
}
}
Err(e) => println!("取得影片列表失敗:{}", e),
}
} else {
println!("\n查詢中...");
match rt.block_on(client.lookup_video(uuid)) {
Ok(info) => {
println!("\n✓ 找到影片:");
println!(" UUID: {}", info.uuid);
if let Some(path) = &info.file_path {
println!(" 路徑:{}", path);
}
if let Some(name) = &info.file_name {
println!(" 名稱:{}", name);
}
if let Some(dur) = info.duration {
println!(" 時長:{:.2}s", dur);
}
}
Err(e) => println!("查詢失敗:{}", e),
}
}
}
"3" => {
println!("\n=== ▶ 播放影片 ===");
print!("輸入影片 UUID (直接 Enter 從列表選擇): ");
input.clear();
std::io::stdin().read_line(&mut input)?;
let uuid = input.trim();
if uuid.is_empty() {
println!("載入影片列表...");
match rt.block_on(client.list_videos()) {
Ok(videos) => {
if videos.is_empty() {
println!("沒有影片");
continue;
}
println!("\n選擇影片:");
for (i, v) in videos.iter().enumerate() {
println!(" [{}] {} ({})", i + 1, v.file_name, v.uuid);
}
print!("\n選擇編號:");
input.clear();
std::io::stdin().read_line(&mut input)?;
if let Ok(idx) = input.trim().parse::<usize>() {
if idx > 0 && idx <= videos.len() {
let selected = &videos[idx - 1];
println!("\n播放: {}", selected.file_path);
if std::path::Path::new(&selected.file_path).exists() {
std::process::Command::new("mpv")
.arg(&selected.file_path)
.spawn()?;
} else {
println!("錯誤:檔案不存在:{}", selected.file_path);
}
}
}
}
Err(e) => println!("取得影片列表失敗:{}", e),
}
} else {
match rt.block_on(client.lookup_video(uuid)) {
Ok(info) => {
if let Some(path) = &info.file_path {
println!("開啟: {}", path);
if std::path::Path::new(path).exists() {
std::process::Command::new("mpv").arg(path).spawn()?;
} else {
println!("錯誤:檔案不存在:{}", path);
}
}
}
Err(e) => println!("查詢失敗:{}", e),
}
}
}
"q" | "Q" => {
println!("\n👋 再見!");
break;
}
_ => {
println!("無效選項");
}
}
}
Ok(())
}
@@ -523,17 +1251,37 @@ fn main() -> Result<()> {
let should_download = args.iter().any(|a| a == "-d" || a == "--download");
let show_selector = args.iter().any(|a| a == "-s" || a == "--selector");
let test_api_mode = args.iter().any(|a| a == "-t" || a == "--test-api");
let local_mode = args.iter().any(|a| a == "-l" || a == "--local");
let online_mode = args.iter().any(|a| a == "-o" || a == "--online");
// Get external player choice
let external_player = args
.iter()
.position(|a| a == "-p" || a == "--player")
.and_then(|i| args.get(i + 1))
.cloned()
.unwrap_or_else(|| "vlc".to_string());
// API Testing Mode
if test_api_mode {
return run_api_test_mode();
}
// If --selector flag is provided, show video selector
// If --selector flag is provided, show video selector (online mode)
if show_selector {
return run_selector();
}
// If --online or -o is provided, run online mode
if online_mode {
return run_online_mode();
}
// If --local or -l is provided, run local mode with external player
if local_mode {
return run_local_mode(&external_player);
}
let video_path = if args.len() < 2 || (should_download && args.len() < 3) {
println!("Video Player\n============\nEnter video path or YouTube URL:");
let mut input = String::new();