mirror of
https://github.com/thegeneralist01/archivr
synced 2026-07-21 18:55:36 +02:00
feat(core+frontend): bare yt:ID resolves to YouTube video
determine_source: yt:ID / youtube:ID with no prefix and exactly 11
chars [A-Za-z0-9_-] → YouTubeVideo via is_youtube_video_id helper.
Reserved prefixes (playlist/, channel/, c/, user/, @) still fire first
so they are unaffected by the fallback.
expand_shorthand_to_url: bare yt:ID expands to watch?v=ID using the
same predicate, consistent with how ytm:ID → music.youtube.com/watch.
isVideoSource (frontend): same 11-char /^[A-Za-z0-9_-]{11}$/ regex so
yt:ID triggers the quality probe and capture-row guards identically to
yt:video/ID.
Tests: test_is_youtube_video_id covers valid IDs (alphanumeric, with _
and -), too-short, too-long, and invalid-char cases using genuinely
invalid fixtures. test_youtube_sources adds bare-ID cases and confirms
reserved prefixes (playlist/, @) are not affected.
This commit is contained in:
parent
e7104cf29d
commit
916146b4a8
4 changed files with 49 additions and 9 deletions
|
|
@ -211,6 +211,12 @@ fn generate_entry_title(source: Source, meta: &PlatformMetadata) -> String {
|
|||
Source::Other => "Archived Content".to_string(),
|
||||
}
|
||||
}
|
||||
/// Returns true when `s` is a valid bare YouTube video ID:
|
||||
/// exactly 11 characters from the set `[A-Za-z0-9_-]`.
|
||||
fn is_youtube_video_id(s: &str) -> bool {
|
||||
s.len() == 11 && s.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')
|
||||
}
|
||||
|
||||
|
||||
fn expand_shorthand_to_url(path: &str, source: &Source) -> String {
|
||||
// YouTube shorthands: yt:video/ID, yt:playlist/ID, yt:@handle, yt:channel/ID, etc.
|
||||
|
|
@ -238,6 +244,10 @@ fn expand_shorthand_to_url(path: &str, source: &Source) -> String {
|
|||
if let Some(handle) = after.strip_prefix("@") {
|
||||
return format!("https://www.youtube.com/@{handle}");
|
||||
}
|
||||
// bare yt:ID — validated 11-char video ID
|
||||
if is_youtube_video_id(after) {
|
||||
return format!("https://www.youtube.com/watch?v={after}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -328,6 +338,11 @@ fn determine_source(path: &str) -> Source {
|
|||
{
|
||||
return Source::YouTubeChannel;
|
||||
}
|
||||
|
||||
// bare yt:ID — exactly 11 chars [A-Za-z0-9_-], no slash/@ prefixes
|
||||
if is_youtube_video_id(after_scheme) {
|
||||
return Source::YouTubeVideo;
|
||||
}
|
||||
}
|
||||
|
||||
// Shorthand scheme: ytm:
|
||||
|
|
@ -2110,6 +2125,15 @@ mod tests {
|
|||
url: "youtube:@CoreDumpped",
|
||||
expected: Source::YouTubeChannel,
|
||||
},
|
||||
// Bare video ID — exactly 11 chars [A-Za-z0-9_-]
|
||||
TestCase { url: "yt:dQw4w9WgXcQ", expected: Source::YouTubeVideo },
|
||||
TestCase { url: "youtube:dQw4w9WgXcQ", expected: Source::YouTubeVideo },
|
||||
TestCase { url: "yt:a_b-c_d-e_4", expected: Source::YouTubeVideo },
|
||||
// Non-ID: wrong length (9 chars) → Other
|
||||
TestCase { url: "yt:not-video", expected: Source::Other },
|
||||
// Reserved prefixes still route correctly when segment looks like an ID
|
||||
TestCase { url: "yt:playlist/dQw4w9WgXcQ", expected: Source::YouTubePlaylist },
|
||||
TestCase { url: "yt:@dQw4w9WgXcQ", expected: Source::YouTubeChannel },
|
||||
];
|
||||
|
||||
for case in &shorthand_cases {
|
||||
|
|
@ -2122,6 +2146,18 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_youtube_video_id() {
|
||||
assert!(is_youtube_video_id("dQw4w9WgXcQ"), "canonical ID");
|
||||
assert!(is_youtube_video_id("a_b-c_d-e_4"), "11-char ID with _ and -");
|
||||
assert!(!is_youtube_video_id(""), "empty");
|
||||
assert!(!is_youtube_video_id("short"), "too short");
|
||||
assert!(!is_youtube_video_id("toolong12345"), "too long (12 chars)");
|
||||
assert!(!is_youtube_video_id("dQw4w9WgXc!"), "invalid char (! at pos 11)");
|
||||
assert!(!is_youtube_video_id("not-video"), "9 chars — too short");
|
||||
assert!(!is_youtube_video_id("dQw4w9WgXC!!"), "12 chars + bad char");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_youtube_music_sources() {
|
||||
// --- determine_source ---
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -4,7 +4,7 @@
|
|||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Archivr</title>
|
||||
<script type="module" crossorigin src="/assets/index-DMYakNuy.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-BlDAGZHO.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-D8ic-z4p.css">
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,11 @@ function isVideoSource(locator) {
|
|||
for (const scheme of ['yt:', 'youtube:']) {
|
||||
if (ll.startsWith(scheme)) {
|
||||
const after = ll.slice(scheme.length)
|
||||
return after.startsWith('video/') || after.startsWith('short/') || after.startsWith('shorts/')
|
||||
if (after.startsWith('video/') || after.startsWith('short/') || after.startsWith('shorts/'))
|
||||
return true
|
||||
// bare yt:ID — exactly 11 chars [A-Za-z0-9_-], same predicate as is_youtube_video_id in core
|
||||
if (/^[a-z0-9_-]{11}$/i.test(after)) return true
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue