From 5ea0c396b8b525a88a8d92b0b63f034b528dc2e2 Mon Sep 17 00:00:00 2001 From: TheGeneralist <180094941+thegeneralist01@users.noreply.github.com> Date: Mon, 22 Jun 2026 22:42:41 +0200 Subject: [PATCH] fix: expand yt:/youtube: shorthands to real URLs before yt-dlp expand_shorthand_to_url handled instagram:/facebook:/tiktok: etc. but not yt:/youtube:. yt-dlp doesn't know the yt: scheme, so shorthands like yt:video/ID failed with 'Unsupported url scheme'. Add YouTube cases for video/, short/, shorts/, playlist/, channel/, c/, user/, and @handle. Full https:// URLs pass through unchanged. Add tests pinning all new expansions. --- crates/archivr-core/src/capture.rs | 58 ++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/crates/archivr-core/src/capture.rs b/crates/archivr-core/src/capture.rs index 19d6a3b..a1690f7 100644 --- a/crates/archivr-core/src/capture.rs +++ b/crates/archivr-core/src/capture.rs @@ -32,6 +32,34 @@ pub struct CaptureResult { } fn expand_shorthand_to_url(path: &str, source: &Source) -> String { + // YouTube shorthands: yt:video/ID, yt:playlist/ID, yt:@handle, yt:channel/ID, etc. + if matches!(source, Source::YouTubeVideo | Source::YouTubePlaylist | Source::YouTubeChannel) { + if let Some(after) = path.strip_prefix("yt:").or_else(|| path.strip_prefix("youtube:")) { + if let Some(id) = after + .strip_prefix("video/") + .or_else(|| after.strip_prefix("short/")) + .or_else(|| after.strip_prefix("shorts/")) + { + return format!("https://www.youtube.com/watch?v={id}"); + } + if let Some(id) = after.strip_prefix("playlist/") { + return format!("https://www.youtube.com/playlist?list={id}"); + } + if let Some(id) = after.strip_prefix("channel/") { + return format!("https://www.youtube.com/channel/{id}"); + } + if let Some(id) = after.strip_prefix("c/") { + return format!("https://www.youtube.com/c/{id}"); + } + if let Some(id) = after.strip_prefix("user/") { + return format!("https://www.youtube.com/user/{id}"); + } + if let Some(handle) = after.strip_prefix("@") { + return format!("https://www.youtube.com/@{handle}"); + } + } + } + if *source == Source::X && (path.starts_with("tweet:media:") || path.starts_with("x:media:")) { if let Some(tweet_id) = path.split(':').next_back().and_then(parse_tweet_id) { return format!("https://x.com/i/status/{tweet_id}"); @@ -830,6 +858,36 @@ mod tests { expand_shorthand_to_url("tweet:1234567890", &Source::Tweet), "tweet:1234567890" ); + // YouTube shorthands must expand to full URLs before yt-dlp sees them + assert_eq!( + expand_shorthand_to_url("yt:video/MntbN1DdEP0", &Source::YouTubeVideo), + "https://www.youtube.com/watch?v=MntbN1DdEP0" + ); + assert_eq!( + expand_shorthand_to_url("yt:shorts/EtC99eWiwRI", &Source::YouTubeVideo), + "https://www.youtube.com/watch?v=EtC99eWiwRI" + ); + assert_eq!( + expand_shorthand_to_url("youtube:video/UHxw-L2WyyY", &Source::YouTubeVideo), + "https://www.youtube.com/watch?v=UHxw-L2WyyY" + ); + assert_eq!( + expand_shorthand_to_url("yt:playlist/PL9vTTBa7QaQO", &Source::YouTubePlaylist), + "https://www.youtube.com/playlist?list=PL9vTTBa7QaQO" + ); + assert_eq!( + expand_shorthand_to_url("yt:@CoreDumpped", &Source::YouTubeChannel), + "https://www.youtube.com/@CoreDumpped" + ); + assert_eq!( + expand_shorthand_to_url("yt:channel/UCxyz123", &Source::YouTubeChannel), + "https://www.youtube.com/channel/UCxyz123" + ); + // Full YouTube URLs pass through unchanged + assert_eq!( + expand_shorthand_to_url("https://www.youtube.com/watch?v=UHxw-L2WyyY", &Source::YouTubeVideo), + "https://www.youtube.com/watch?v=UHxw-L2WyyY" + ); } #[test]