fix: resolve omitted search handles before saving channels

This commit is contained in:
2026-10-11 23:32:00 +02:00
parent b53b57ee17
commit 6563a3d38a
3 changed files with 145 additions and 14 deletions
+55
View File
@@ -196,6 +196,25 @@ pub fn full_description(output: &str, expected_id: &str) -> Result<String, Strin
Ok(description)
}
pub fn channel_handle(output: &str, expected_id: &str) -> Result<String, String> {
#[derive(Deserialize)]
struct Channel {
channel_id: String,
uploader_id: Option<String>,
}
let channel: Channel = serde_json::from_str(output)
.map_err(|error| format!("Invalid channel metadata: {error}"))?;
if channel.channel_id != expected_id {
return Err("Channel metadata did not match the requested channel".into());
}
let handle = channel
.uploader_id
.filter(|handle| handle.starts_with('@'))
.ok_or_else(|| "This channel has no available handle; no channel was saved.".to_owned())?;
crate::config::normalize_channel(&handle).map_err(|error| error.to_string())?;
Ok(handle)
}
fn page_end(start: usize) -> io::Result<usize> {
start
.checked_add(PAGE_SIZE - 1)
@@ -251,6 +270,22 @@ impl Job {
)
}
pub fn channel(id: &str) -> io::Result<Self> {
if !valid_channel_id(id) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Invalid YouTube channel ID",
));
}
Self::spawn(extractor().args([
"--flat-playlist",
"--playlist-end",
"1",
"--dump-single-json",
&format!("https://www.youtube.com/channel/{id}/videos"),
]))
}
pub fn description(id: &str) -> io::Result<Self> {
Self::spawn(extractor().args([
"--no-playlist",
@@ -839,6 +874,26 @@ mod tests {
assert_eq!(exhausted.videos.len(), 2);
}
#[test]
fn channel_handles_require_the_requested_identity_and_a_valid_handle() {
let id = "UChAQQ9at2MDlXjvQX3vL3Yw";
for handle in ["@ch.1676", "@敷嶋てとら"] {
let metadata = serde_json::json!({"channel_id": id, "uploader_id": handle}).to_string();
assert_eq!(channel_handle(&metadata, id).unwrap(), handle);
assert!(channel_handle(&metadata, "UCaYhcUwRBNscFNUKTjgPFiA").is_err());
}
for handle in [
None,
Some("ch.1676"),
Some("@bad/handle"),
Some("@bad\u{1b}"),
] {
let metadata = serde_json::json!({"channel_id": id, "uploader_id": handle}).to_string();
assert!(channel_handle(&metadata, id).is_err());
}
assert!(channel_handle("not JSON", id).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."}"#;