feat: add full descriptions in a scrollable popup
This commit is contained in:
+52
-2
@@ -13,6 +13,9 @@ pub struct Video {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub channel: Option<String>,
|
||||
// Search descriptions are truncated; only full video metadata populates this cache.
|
||||
#[serde(skip)]
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -42,6 +45,22 @@ pub fn search_results(output: &str) -> Result<Vec<Video>, String> {
|
||||
Ok(videos)
|
||||
}
|
||||
|
||||
pub fn full_description(output: &str, expected_id: &str) -> Result<String, String> {
|
||||
#[derive(Deserialize)]
|
||||
struct Details {
|
||||
id: String,
|
||||
description: Option<String>,
|
||||
}
|
||||
let details: Details =
|
||||
serde_json::from_str(output).map_err(|error| format!("Invalid video metadata: {error}"))?;
|
||||
if details.id != expected_id {
|
||||
return Err("Video metadata did not match the requested video".into());
|
||||
}
|
||||
let mut description = details.description.unwrap_or_default();
|
||||
description.retain(|c| !c.is_control() || c == '\n' || c == '\t');
|
||||
Ok(description)
|
||||
}
|
||||
|
||||
// Own the extractor so replacing a request or quitting also cancels its process.
|
||||
pub struct Job {
|
||||
child: Child,
|
||||
@@ -50,9 +69,25 @@ pub struct Job {
|
||||
|
||||
impl Job {
|
||||
pub fn search(query: &str) -> io::Result<Self> {
|
||||
Self::spawn(&[
|
||||
"--flat-playlist",
|
||||
"--dump-single-json",
|
||||
&format!("ytsearch20:{query}"),
|
||||
])
|
||||
}
|
||||
|
||||
pub fn description(id: &str) -> io::Result<Self> {
|
||||
Self::spawn(&[
|
||||
"--no-playlist",
|
||||
"--skip-download",
|
||||
"--dump-single-json",
|
||||
&format!("https://www.youtube.com/watch?v={id}"),
|
||||
])
|
||||
}
|
||||
|
||||
fn spawn(args: &[&str]) -> io::Result<Self> {
|
||||
let mut child = extractor()
|
||||
.args(["--flat-playlist", "--dump-single-json"])
|
||||
.arg(format!("ytsearch20:{query}"))
|
||||
.args(args)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
@@ -265,4 +300,19 @@ mod tests {
|
||||
assert!(search_results(r#"{"entries":[{"id":"--bad","title":"Bad"}]}"#).is_err());
|
||||
assert!(search_results("not JSON").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_descriptions_preserve_paragraphs_and_reject_mismatched_videos() {
|
||||
let metadata = r#"{"id":"jNQXAC9IVRw","description":"First paragraph.\n\n日本語\tsecond paragraph.\u001b\nLast line."}"#;
|
||||
assert_eq!(
|
||||
full_description(metadata, "jNQXAC9IVRw").unwrap(),
|
||||
"First paragraph.\n\n日本語\tsecond paragraph.\nLast line."
|
||||
);
|
||||
assert!(full_description(metadata, "rQ_J9WH6CGk").is_err());
|
||||
assert!(full_description("not JSON", "jNQXAC9IVRw").is_err());
|
||||
assert_eq!(
|
||||
full_description(r#"{"id":"jNQXAC9IVRw","description":null}"#, "jNQXAC9IVRw").unwrap(),
|
||||
""
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user