feat: save readable channel handles with stable URL mappings
This commit is contained in:
+24
-3
@@ -14,6 +14,7 @@ pub struct Video {
|
||||
pub title: String,
|
||||
pub channel: Option<String>,
|
||||
pub channel_id: Option<String>,
|
||||
pub uploader_id: Option<String>,
|
||||
pub timestamp: Option<i64>,
|
||||
// Search descriptions are truncated; only full video metadata populates this cache.
|
||||
#[serde(skip)]
|
||||
@@ -25,6 +26,7 @@ struct SearchResults {
|
||||
entries: Vec<Option<Video>>,
|
||||
channel: Option<String>,
|
||||
channel_id: Option<String>,
|
||||
uploader_id: Option<String>,
|
||||
}
|
||||
|
||||
pub fn search_results(output: &str) -> Result<Vec<Video>, String> {
|
||||
@@ -63,6 +65,13 @@ fn validate_video(video: &mut Video) -> Result<(), String> {
|
||||
{
|
||||
return Err("an invalid YouTube channel ID".into());
|
||||
}
|
||||
if video
|
||||
.uploader_id
|
||||
.as_deref()
|
||||
.is_some_and(|handle| handle.chars().any(char::is_control))
|
||||
{
|
||||
return Err("an invalid YouTube channel handle".into());
|
||||
}
|
||||
video.title.retain(|c| !c.is_control());
|
||||
if let Some(channel) = &mut video.channel {
|
||||
channel.retain(|c| !c.is_control());
|
||||
@@ -88,6 +97,9 @@ pub fn feed_results(output: &str) -> Result<Vec<Video>, String> {
|
||||
if video.channel_id.is_none() {
|
||||
video.channel_id = playlist.channel_id.clone();
|
||||
}
|
||||
if video.uploader_id.is_none() {
|
||||
video.uploader_id = playlist.uploader_id.clone();
|
||||
}
|
||||
if video.channel.is_none() {
|
||||
video.channel = playlist.channel.clone();
|
||||
}
|
||||
@@ -485,26 +497,30 @@ mod tests {
|
||||
#[test]
|
||||
fn search_handles_unavailable_entries_and_rejects_invalid_ids() {
|
||||
let videos = search_results(
|
||||
r#"{"entries":[null,{"id":"rQ_J9WH6CGk","title":"Rust\u001b","channel":null}]}"#,
|
||||
r#"{"entries":[null,{"id":"rQ_J9WH6CGk","title":"Rust\u001b","channel":null,"uploader_id":"@RustVideos"}]}"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(videos[0].title, "Rust");
|
||||
assert_eq!(videos[0].channel, None);
|
||||
assert_eq!(videos[0].channel_id, None);
|
||||
assert_eq!(videos[0].uploader_id.as_deref(), Some("@RustVideos"));
|
||||
assert!(
|
||||
search_results(
|
||||
r#"{"entries":[{"id":"rQ_J9WH6CGk","title":"Bad","channel_id":"not-a-channel"}]}"#
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
assert!(search_results(
|
||||
r#"{"entries":[{"id":"jNQXAC9IVRw","title":"Unsafe","uploader_id":"@jawed\u001b"}]}"#
|
||||
).is_err());
|
||||
assert!(search_results(r#"{"entries":[{"id":"--bad","title":"Bad"}]}"#).is_err());
|
||||
assert!(search_results("not JSON").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn feeds_merge_newest_first_deduplicate_and_inherit_channel_identity() {
|
||||
let output = r#"{"channel_id":"UCaYhcUwRBNscFNUKTjgPFiA","channel":"Rust\u001b","entries":[null,{"id":"l-GaaivVJCE","title":"Older","timestamp":100},{"id":"jNQXAC9IVRw","title":"Unknown","timestamp":null}]}
|
||||
{"channel_id":"UCaYhcUwRBNscFNUKTjgPFiA","channel":"Rust","entries":[{"id":"SLV9AJxnb1U","title":"Newest","timestamp":300},{"id":"l-GaaivVJCE","title":"Newer","timestamp":200},{"id":"rQ_J9WH6CGk","title":"No metadata","channel_id":null}]}"#;
|
||||
let output = r#"{"channel_id":"UCaYhcUwRBNscFNUKTjgPFiA","uploader_id":"@RustVideos","channel":"Rust\u001b","entries":[null,{"id":"l-GaaivVJCE","title":"Older","timestamp":100},{"id":"jNQXAC9IVRw","title":"Unknown","timestamp":null}]}
|
||||
{"channel_id":"UCaYhcUwRBNscFNUKTjgPFiA","uploader_id":"@RustVideos","channel":"Rust","entries":[{"id":"SLV9AJxnb1U","title":"Newest","timestamp":300},{"id":"l-GaaivVJCE","title":"Newer","timestamp":200},{"id":"rQ_J9WH6CGk","title":"No metadata","channel_id":null}]}"#;
|
||||
let videos = feed_results(output).unwrap();
|
||||
assert_eq!(
|
||||
videos
|
||||
@@ -522,6 +538,11 @@ mod tests {
|
||||
.iter()
|
||||
.all(|video| video.channel_id.as_deref() == Some("UCaYhcUwRBNscFNUKTjgPFiA"))
|
||||
);
|
||||
assert!(
|
||||
videos
|
||||
.iter()
|
||||
.all(|video| video.uploader_id.as_deref() == Some("@RustVideos"))
|
||||
);
|
||||
let unavailable = feed_results(
|
||||
r#"{"entries":[null,{"id":"jNQXAC9IVRw","title":"Unknown","channel_id":null}]}"#,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user