Phase 6 — Search + Channel + Playlist + LinkHandler
Pulls in the read-side extractor surfaces Straw needs at app open
(search bar) + on detail screens (channel + playlist).
src/youtube/linkhandler/
* mod.rs — ACCEPTED_HOSTS allowlist (youtube.com /
youtube-nocookie.com / youtu.be / m.youtube.com /
music.youtube.com); 27 Invidious mirror hosts
intentionally dropped (SPEC §6.6).
* stream.rs — extract_video_id() handles /watch?v= / youtu.be/ /
/embed/ / /shorts/ / /v/ / /live/ / attribution_link;
strict 11-char [A-Za-z0-9_-] validation.
* channel.rs — ChannelIdentifier enum (DirectId / Handle / Custom /
LegacyUser). Resolution to UC… id lands in
youtube/channel.rs.
* playlist.rs — extracts ?list=<PLid> from /playlist and /watch URLs.
* search.rs — SearchFilter enum + params() opaque base64 strings +
uses_music_endpoint() routing flag.
src/youtube/search_extractor.rs
* search(query, filter) → SearchInfo { query, corrected_query,
videos, continuation_token }
* Walks twoColumnSearchResultsRenderer → sectionListRenderer →
itemSectionRenderer → videoRenderer (+ shelfRenderer recursion).
* Parses YT duration strings, view-count abbreviations ('1.5M views'),
publishedTimeText, ownerBadges verified flag, badge LIVE flag.
* Music-search filters route to WEB_REMIX — flagged as not-yet-impl.
src/youtube/suggestion_extractor.rs
* suggestions(query) → Vec<String> via the suggestqueries-clients6
endpoint; handles both XSSI-prefixed and bare JSON responses.
src/youtube/channel.rs
* resolve_handle_to_channel_id() via /youtubei/v1/navigation/resolve_url
* channel_info(ChannelIdentifier) → ChannelInfo {
name, description, avatars, banners, subscriber_count, verified,
recent_videos, videos_continuation
}
* Parses both c4TabbedHeaderRenderer (most common) and the newer
pageHeaderRenderer flavor.
* subscriber_count parser handles K/M/B suffixes.
src/youtube/playlist_extractor.rs
* playlist_info(playlist_id) → PlaylistInfo with first-page video
list + continuation_token. Browses with browseId='VL<id>'.
* Walks playlistMetadataRenderer + playlistSidebarRenderer + the
playlistVideoListRenderer.contents[] for video items.
Tests: 121 lib unit pass (+44 since Phase 5). All previous phase smoke
tests still green.
What's left:
* Phase 6 kiosks (Trending etc) — minor, deferred
* Phase 7 — UniFFI surface swap into Straw (Straw repo work)
* Phase 8 — delete rustypipe (Straw repo work)
This commit is contained in:
parent
1533157485
commit
0a2d79c743
10 changed files with 1663 additions and 0 deletions
70
src/youtube/linkhandler/mod.rs
Normal file
70
src/youtube/linkhandler/mod.rs
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
// LinkHandler factories — URL parsing + URL building for YouTube
|
||||
// resource categories. Mirrors NPE
|
||||
// services/youtube/linkHandler/Youtube*LinkHandlerFactory.java.
|
||||
//
|
||||
// PORT SCOPE (per SPEC §6.6): we keep youtube.com / youtube-nocookie.com
|
||||
// / youtu.be / m.youtube.com / music.youtube.com. The 27-host Invidious
|
||||
// mirror list in NPE is dropped — Sulkta isn't an Invidious mirror.
|
||||
|
||||
pub mod channel;
|
||||
pub mod playlist;
|
||||
pub mod search;
|
||||
pub mod stream;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum LinkError {
|
||||
#[error("invalid url: {0}")]
|
||||
InvalidUrl(String),
|
||||
#[error("unsupported host: {0}")]
|
||||
UnsupportedHost(String),
|
||||
#[error("missing id in url: {0}")]
|
||||
MissingId(String),
|
||||
#[error("malformed id: {0}")]
|
||||
MalformedId(String),
|
||||
}
|
||||
|
||||
/// The acceptable hosts for first-party YT links. Audit Track D §6.
|
||||
pub const ACCEPTED_HOSTS: &[&str] = &[
|
||||
"youtube.com",
|
||||
"www.youtube.com",
|
||||
"m.youtube.com",
|
||||
"music.youtube.com",
|
||||
"youtu.be",
|
||||
"www.youtube-nocookie.com",
|
||||
];
|
||||
|
||||
pub fn host_is_youtube(host: &str) -> bool {
|
||||
let h = host.to_ascii_lowercase();
|
||||
let h = h.strip_prefix("www.").unwrap_or(&h);
|
||||
ACCEPTED_HOSTS
|
||||
.iter()
|
||||
.any(|allowed| {
|
||||
let allowed = allowed.strip_prefix("www.").unwrap_or(allowed);
|
||||
allowed == h
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn accepts_first_party_hosts() {
|
||||
assert!(host_is_youtube("www.youtube.com"));
|
||||
assert!(host_is_youtube("youtube.com"));
|
||||
assert!(host_is_youtube("m.youtube.com"));
|
||||
assert!(host_is_youtube("music.youtube.com"));
|
||||
assert!(host_is_youtube("youtu.be"));
|
||||
assert!(host_is_youtube("WWW.YouTube.COM")); // case-insensitive
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invidious_and_random() {
|
||||
assert!(!host_is_youtube("invidious.io"));
|
||||
assert!(!host_is_youtube("yewtu.be"));
|
||||
assert!(!host_is_youtube("piped.video"));
|
||||
assert!(!host_is_youtube("evil.com"));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue