use crate::errors::{CouldNotRetrieveTranscript, CouldNotRetrieveTranscriptReason};
pub struct PlayabilityAsserter;
impl PlayabilityAsserter {
pub fn assert_playability(
player_data: &serde_json::Value,
video_id: &str,
) -> Result<(), CouldNotRetrieveTranscript> {
let status = player_data
.get("playabilityStatus")
.and_then(|s| s.get("status"))
.and_then(|s| s.as_str())
.unwrap_or("ERROR");
match status {
"OK" => Ok(()),
"LOGIN_REQUIRED" => {
let reason = player_data
.get("playabilityStatus")
.and_then(|s| s.get("reason"))
.and_then(|s| s.as_str())
.unwrap_or("");
if reason.contains("age") {
Err(CouldNotRetrieveTranscript {
video_id: video_id.to_string(),
reason: Some(CouldNotRetrieveTranscriptReason::AgeRestricted),
})
} else {
let mut sub_reasons = Vec::new();
if let Some(messages) = player_data
.get("playabilityStatus")
.and_then(|s| s.get("errorScreen"))
.and_then(|s| s.get("playerErrorMessageRenderer"))
.and_then(|s| s.get("subreason"))
.and_then(|s| s.get("runs"))
.and_then(|s| s.as_array())
{
for msg in messages {
if let Some(text) = msg.get("text").and_then(|t| t.as_str()) {
sub_reasons.push(text.to_string());
}
}
}
Err(CouldNotRetrieveTranscript {
video_id: video_id.to_string(),
reason: Some(CouldNotRetrieveTranscriptReason::VideoUnplayable {
reason: Some(reason.to_string()),
sub_reasons,
}),
})
}
}
_ => {
let reason = player_data
.get("playabilityStatus")
.and_then(|s| s.get("reason"))
.and_then(|s| s.as_str())
.unwrap_or("");
if reason.contains("Video unavailable") {
Err(CouldNotRetrieveTranscript {
video_id: video_id.to_string(),
reason: Some(CouldNotRetrieveTranscriptReason::VideoUnavailable),
})
} else {
let mut sub_reasons = Vec::new();
if let Some(messages) = player_data
.get("playabilityStatus")
.and_then(|s| s.get("errorScreen"))
.and_then(|s| s.get("playerErrorMessageRenderer"))
.and_then(|s| s.get("subreason"))
.and_then(|s| s.get("runs"))
.and_then(|s| s.as_array())
{
for msg in messages {
if let Some(text) = msg.get("text").and_then(|t| t.as_str()) {
sub_reasons.push(text.to_string());
}
}
}
Err(CouldNotRetrieveTranscript {
video_id: video_id.to_string(),
reason: Some(CouldNotRetrieveTranscriptReason::VideoUnplayable {
reason: Some(reason.to_string()),
sub_reasons,
}),
})
}
}
}
}
pub fn extract_subreasons(player_data: &serde_json::Value) -> Vec<String> {
let mut sub_reasons = Vec::new();
if let Some(messages) = player_data
.get("playabilityStatus")
.and_then(|s| s.get("errorScreen"))
.and_then(|s| s.get("playerErrorMessageRenderer"))
.and_then(|s| s.get("subreason"))
.and_then(|s| s.get("runs"))
.and_then(|s| s.as_array())
{
for msg in messages {
if let Some(text) = msg.get("text").and_then(|t| t.as_str()) {
sub_reasons.push(text.to_string());
}
}
}
sub_reasons
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_playable_video() {
let video_id = "dQw4w9WgXcQ";
let player_data = json!({
"playabilityStatus": {
"status": "OK"
}
});
let result = PlayabilityAsserter::assert_playability(&player_data, video_id);
assert!(result.is_ok());
}
#[test]
fn test_age_restricted_video() {
let video_id = "age_restricted_video";
let player_data = json!({
"playabilityStatus": {
"status": "LOGIN_REQUIRED",
"reason": "This video may be inappropriate for some users. Sign in to confirm your age."
}
});
let result = PlayabilityAsserter::assert_playability(&player_data, video_id);
assert!(result.is_err());
let error = result.unwrap_err();
assert_eq!(error.video_id, video_id);
assert!(matches!(
error.reason,
Some(CouldNotRetrieveTranscriptReason::AgeRestricted)
));
}
#[test]
fn test_unavailable_video() {
let video_id = "unavailable_video";
let player_data = json!({
"playabilityStatus": {
"status": "ERROR",
"reason": "Video unavailable",
"errorScreen": {
"playerErrorMessageRenderer": {
"subreason": {
"runs": [
{"text": "This video is no longer available"}
]
}
}
}
}
});
let result = PlayabilityAsserter::assert_playability(&player_data, video_id);
assert!(result.is_err());
let error = result.unwrap_err();
assert_eq!(error.video_id, video_id);
assert!(matches!(
error.reason,
Some(CouldNotRetrieveTranscriptReason::VideoUnavailable)
));
}
#[test]
fn test_unplayable_video() {
let video_id = "unplayable_video";
let player_data = json!({
"playabilityStatus": {
"status": "ERROR",
"reason": "This video is not available in your country.",
"errorScreen": {
"playerErrorMessageRenderer": {
"subreason": {
"runs": [
{"text": "Due to copyright restrictions"},
{"text": "Try using a VPN"}
]
}
}
}
}
});
let result = PlayabilityAsserter::assert_playability(&player_data, video_id);
assert!(result.is_err());
let error = result.unwrap_err();
assert_eq!(error.video_id, video_id);
match error.reason {
Some(CouldNotRetrieveTranscriptReason::VideoUnplayable {
reason,
sub_reasons,
}) => {
assert_eq!(
reason,
Some("This video is not available in your country.".to_string())
);
assert_eq!(sub_reasons.len(), 2);
assert_eq!(sub_reasons[0], "Due to copyright restrictions");
assert_eq!(sub_reasons[1], "Try using a VPN");
}
_ => panic!(
"Expected VideoUnplayable reason but got: {:?}",
error.reason
),
}
}
#[test]
fn test_login_required_non_age() {
let video_id = "premium_video";
let player_data = json!({
"playabilityStatus": {
"status": "LOGIN_REQUIRED",
"reason": "This is a premium video. Please sign in to watch."
}
});
let result = PlayabilityAsserter::assert_playability(&player_data, video_id);
assert!(result.is_err());
let error = result.unwrap_err();
assert_eq!(error.video_id, video_id);
match error.reason {
Some(CouldNotRetrieveTranscriptReason::VideoUnplayable {
reason,
sub_reasons,
}) => {
assert_eq!(
reason,
Some("This is a premium video. Please sign in to watch.".to_string())
);
assert!(sub_reasons.is_empty());
}
_ => panic!(
"Expected VideoUnplayable reason but got: {:?}",
error.reason
),
}
}
#[test]
fn test_missing_playability_status() {
let video_id = "missing_status";
let player_data = json!({
"otherData": {
"something": "value"
}
});
let result = PlayabilityAsserter::assert_playability(&player_data, video_id);
assert!(result.is_err());
let error = result.unwrap_err();
assert_eq!(error.video_id, video_id);
match error.reason {
Some(CouldNotRetrieveTranscriptReason::VideoUnplayable {
reason,
sub_reasons,
}) => {
assert_eq!(reason, Some("".to_string()));
assert!(sub_reasons.is_empty());
}
_ => panic!(
"Expected VideoUnplayable reason but got: {:?}",
error.reason
),
}
}
#[test]
fn test_extract_subreasons_empty() {
let player_data = json!({
"playabilityStatus": {
"status": "ERROR",
"reason": "Some error"
}
});
let reasons = PlayabilityAsserter::extract_subreasons(&player_data);
assert!(reasons.is_empty());
}
#[test]
fn test_extract_subreasons_single() {
let player_data = json!({
"playabilityStatus": {
"status": "ERROR",
"reason": "Some error",
"errorScreen": {
"playerErrorMessageRenderer": {
"subreason": {
"runs": [
{"text": "This is the only reason"}
]
}
}
}
}
});
let reasons = PlayabilityAsserter::extract_subreasons(&player_data);
assert_eq!(reasons.len(), 1);
assert_eq!(reasons[0], "This is the only reason");
}
#[test]
fn test_extract_subreasons_multiple() {
let player_data = json!({
"playabilityStatus": {
"status": "ERROR",
"reason": "Some error",
"errorScreen": {
"playerErrorMessageRenderer": {
"subreason": {
"runs": [
{"text": "First reason"},
{"text": "Second reason"},
{"text": "Third reason"}
]
}
}
}
}
});
let reasons = PlayabilityAsserter::extract_subreasons(&player_data);
assert_eq!(reasons.len(), 3);
assert_eq!(reasons[0], "First reason");
assert_eq!(reasons[1], "Second reason");
assert_eq!(reasons[2], "Third reason");
}
#[test]
fn test_extract_subreasons_malformed() {
let player_data = json!({
"playabilityStatus": {
"status": "ERROR",
"reason": "Some error",
"errorScreen": {
"playerErrorMessageRenderer": {
"subreason": {
"runs": [
{"not_text": "This should be skipped"},
{"text": "This should be included"},
null,
{"text": null},
{"text": "This should also be included"}
]
}
}
}
}
});
let reasons = PlayabilityAsserter::extract_subreasons(&player_data);
assert_eq!(reasons.len(), 2);
assert_eq!(reasons[0], "This should be included");
assert_eq!(reasons[1], "This should also be included");
}
#[test]
fn test_different_error_status() {
let video_id = "different_error";
let player_data = json!({
"playabilityStatus": {
"status": "CONTENT_CHECK_REQUIRED",
"reason": "The following content may not be appropriate for some users."
}
});
let result = PlayabilityAsserter::assert_playability(&player_data, video_id);
assert!(result.is_err());
let error = result.unwrap_err();
assert_eq!(error.video_id, video_id);
match error.reason {
Some(CouldNotRetrieveTranscriptReason::VideoUnplayable { reason, .. }) => {
assert_eq!(
reason,
Some(
"The following content may not be appropriate for some users.".to_string()
)
);
}
_ => panic!(
"Expected VideoUnplayable reason but got: {:?}",
error.reason
),
}
}
}