chore: stage all pending work — caption styling, media server, processing modal, docs, summaries
Includes: - Extended caption styling (font, shadow, dimmed color, bg toggle) - Media server, subtitle downloader, VTT parser, processing modal - Waveform tiers, thumbnail/timeline improvements, transport controls - Hybrid download model, dependency management, clip export enhancements - 21 chat summaries, 2 implementation plans, 2 design specs Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,13 +1,56 @@
|
||||
use crate::models::{CookieSource, VideoMetadata};
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::process::Command;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SubtitleFormat {
|
||||
pub ext: Option<String>,
|
||||
pub url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct YtDlpJson {
|
||||
pub title: String,
|
||||
pub duration: Option<f64>,
|
||||
pub fps: Option<f64>,
|
||||
pub thumbnail: Option<String>,
|
||||
/// Manually authored subtitles: language_code -> [formats]
|
||||
#[serde(default)]
|
||||
pub subtitles: HashMap<String, Vec<SubtitleFormat>>,
|
||||
/// Auto-generated subtitles: language_code -> [formats]
|
||||
#[serde(default)]
|
||||
pub automatic_captions: HashMap<String, Vec<SubtitleFormat>>,
|
||||
}
|
||||
|
||||
/// Check if subtitles have a VTT format for English (en, en-US, en-GB, en-orig).
|
||||
fn find_english_key(map: &HashMap<String, Vec<SubtitleFormat>>) -> Option<&str> {
|
||||
// Prefer exact "en", then any en-* variant
|
||||
for key in ["en", "en-US", "en-GB", "en-orig"] {
|
||||
if map.contains_key(key) {
|
||||
return Some(key);
|
||||
}
|
||||
}
|
||||
// Try any key starting with "en"
|
||||
for key in map.keys() {
|
||||
if key.starts_with("en") {
|
||||
return Some(key.as_str());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Determine caption availability: (has_captions, is_auto)
|
||||
pub fn detect_captions(meta: &YtDlpJson) -> (bool, bool) {
|
||||
// Priority 1: manual English subtitles
|
||||
if find_english_key(&meta.subtitles).is_some() {
|
||||
return (true, false);
|
||||
}
|
||||
// Priority 2: auto-generated English captions
|
||||
if find_english_key(&meta.automatic_captions).is_some() {
|
||||
return (true, true);
|
||||
}
|
||||
(false, false)
|
||||
}
|
||||
|
||||
pub fn parse_ytdlp_json(json_str: &str) -> Result<YtDlpJson, String> {
|
||||
@@ -41,6 +84,8 @@ pub fn resolve(url: &str, cookie_source: &CookieSource) -> Result<VideoMetadata,
|
||||
let json_str = String::from_utf8_lossy(&meta_output.stdout);
|
||||
let meta = parse_ytdlp_json(&json_str)?;
|
||||
|
||||
let (has_captions, captions_are_auto) = detect_captions(&meta);
|
||||
|
||||
let mut stream_cmd = Command::new("yt-dlp");
|
||||
stream_cmd
|
||||
.arg("-g")
|
||||
@@ -79,6 +124,8 @@ pub fn resolve(url: &str, cookie_source: &CookieSource) -> Result<VideoMetadata,
|
||||
fps: meta.fps.unwrap_or(30.0),
|
||||
thumbnail_url: meta.thumbnail,
|
||||
stream_url,
|
||||
has_captions,
|
||||
captions_are_auto,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -107,6 +154,8 @@ mod tests {
|
||||
assert!(result.duration.is_none());
|
||||
assert!(result.fps.is_none());
|
||||
assert!(result.thumbnail.is_none());
|
||||
assert!(result.subtitles.is_empty());
|
||||
assert!(result.automatic_captions.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -114,4 +163,39 @@ mod tests {
|
||||
let result = parse_ytdlp_json("not json");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_captions_manual() {
|
||||
let json = r#"{"title": "T", "subtitles": {"en": [{"ext": "vtt", "url": "http://x"}]}, "automatic_captions": {}}"#;
|
||||
let meta = parse_ytdlp_json(json).unwrap();
|
||||
let (has, is_auto) = detect_captions(&meta);
|
||||
assert!(has);
|
||||
assert!(!is_auto);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_captions_auto_only() {
|
||||
let json = r#"{"title": "T", "subtitles": {}, "automatic_captions": {"en": [{"ext": "vtt", "url": "http://x"}]}}"#;
|
||||
let meta = parse_ytdlp_json(json).unwrap();
|
||||
let (has, is_auto) = detect_captions(&meta);
|
||||
assert!(has);
|
||||
assert!(is_auto);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_captions_none() {
|
||||
let json = r#"{"title": "T"}"#;
|
||||
let meta = parse_ytdlp_json(json).unwrap();
|
||||
let (has, _) = detect_captions(&meta);
|
||||
assert!(!has);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_captions_en_variant() {
|
||||
let json = r#"{"title": "T", "subtitles": {"en-US": [{"ext": "vtt"}]}}"#;
|
||||
let meta = parse_ytdlp_json(json).unwrap();
|
||||
let (has, is_auto) = detect_captions(&meta);
|
||||
assert!(has);
|
||||
assert!(!is_auto);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user