diff --git a/src/client/player.rs b/src/client/player.rs index fe28dca..d84adfb 100644 --- a/src/client/player.rs +++ b/src/client/player.rs @@ -12,8 +12,8 @@ use crate::{ deobfuscate::Deobfuscator, error::{internal::DeobfError, Error, ExtractionError, UnavailabilityReason}, model::{ - traits::QualityOrd, AudioCodec, AudioFormat, AudioStream, AudioTrack, ChannelId, Subtitle, - VideoCodec, VideoFormat, VideoPlayer, VideoPlayerDetails, VideoStream, + traits::QualityOrd, AudioCodec, AudioFormat, AudioStream, AudioTrack, ChannelId, Frameset, + Subtitle, VideoCodec, VideoFormat, VideoPlayer, VideoPlayerDetails, VideoStream, }, param::Language, util, @@ -313,6 +313,54 @@ impl MapResponse for response::Player { .collect() }); + let preview_frames = self + .storyboards + .and_then(|sb| { + let spec = sb.player_storyboard_spec_renderer.spec; + let mut spec_parts = spec.split('|'); + let url_tmpl = spec_parts.next()?; + + Some( + spec_parts + .enumerate() + .filter_map(|(i, fs_spec)| { + // Example: 160#90#131#5#5#2000#M$M#rs$AOn4CLCV3TJ2Nty5fbw2r-Lqg4VDOZcVvQ + let mut parts = fs_spec.split('#'); + + let frame_width = parts.next()?.parse().ok()?; + let frame_height = parts.next()?.parse().ok()?; + let total_count = parts.next()?.parse().ok()?; + let frames_per_page_x = parts.next()?.parse().ok()?; + let frames_per_page_y = parts.next()?.parse().ok()?; + let duration_per_frame = parts.next()?.parse().ok()?; + + let n = parts.next()?; + let sigh = parts.next()?; + + let url = url_tmpl.replace("$L", &i.to_string()).replace("$N", n) + + "&sigh=" + + sigh; + + let sprite_count = ((total_count as f64) + / (frames_per_page_x * frames_per_page_y) as f64) + .ceil() as u32; + + Some(Frameset { + url_template: url, + frame_width, + frame_height, + page_count: sprite_count, + total_count, + duration_per_frame, + frames_per_page_x, + frames_per_page_y, + }) + }) + .collect(), + ) + }) + .unwrap_or_default(); + Ok(MapResult { c: VideoPlayer { details: video_info, @@ -323,6 +371,7 @@ impl MapResponse for response::Player { expires_in_seconds: streaming_data.expires_in_seconds, hls_manifest_url: streaming_data.hls_manifest_url, dash_manifest_url: streaming_data.dash_manifest_url, + preview_frames, visitor_data: self.response_context.visitor_data, }, warnings, diff --git a/src/client/response/player.rs b/src/client/response/player.rs index a801a3a..0de004d 100644 --- a/src/client/response/player.rs +++ b/src/client/response/player.rs @@ -7,6 +7,7 @@ use serde_with::{json::JsonString, DefaultOnError}; use super::{ResponseContext, Thumbnails}; use crate::serializer::{text::Text, MapResult}; +#[serde_as] #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct Player { @@ -14,6 +15,9 @@ pub(crate) struct Player { pub streaming_data: Option, pub captions: Option, pub video_details: Option, + #[serde(default)] + #[serde_as(deserialize_as = "DefaultOnError")] + pub storyboards: Option, pub response_context: ResponseContext, } @@ -246,3 +250,15 @@ pub(crate) struct VideoDetails { pub author: String, pub is_live_content: bool, } + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct Storyboards { + pub player_storyboard_spec_renderer: StoryboardRenderer, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct StoryboardRenderer { + pub spec: String, +} diff --git a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_android.snap b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_android.snap index 98168f1..696c919 100644 --- a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_android.snap +++ b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_android.snap @@ -450,5 +450,37 @@ VideoPlayer( expires_in_seconds: 21540, hls_manifest_url: None, dash_manifest_url: Some("https://manifest.googlevideo.com/api/manifest/dash/expire/1659481355/ei/q1jpYtOPEYSBgQeHmqbwAQ/ip/2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e/id/a4fbddf14c6649b4/source/youtube/requiressl/yes/playback_host/rr5---sn-h0jeenek.googlevideo.com/mh/mQ/mm/31%2C29/mn/sn-h0jeenek%2Csn-h0jelnez/ms/au%2Crdu/mv/m/mvi/5/pl/37/hfr/1/as/fmp4_audio_clear%2Cfmp4_sd_hd_clear/initcwndbps/1527500/vprv/1/mt/1659459429/fvip/4/itag_bl/376%2C377%2C384%2C385%2C612%2C613%2C617%2C619%2C623%2C628%2C655%2C656%2C660%2C662%2C666%2C671/keepalive/yes/fexp/24001373%2C24007246/itag/0/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cas%2Cvprv%2Citag/sig/AOq0QJ8wRAIgMm4a_MIHA3YUszKeruSy3exs5JwNjJAyLAwxL0yPdNMCIANb9GDMSTp_NT-PPhbvYMwRULJ5a9BO6MYD9FuWprC1/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgETSOwhwWVMy7gmrFXZlJu655ToLzSwOEsT16oRyrWhACIQDkvOEw1fImz5omu4iVIRNFe-z-JC9v8WUyx281dW2NOw%3D%3D"), + preview_frames: [ + Frameset( + url_template: "https://i.ytimg.com/sb/pPvd8UxmSbQ/storyboard3_L0/default.jpg?sqp=-oaymwENSDfyq4qpAwVwAcABBqLzl_8DBgjf8LPxBQ==&sigh=rs$AOn4CLAXobPyrylgm8IEvjlZzqYTiPe1Ow", + frame_width: 48, + frame_height: 27, + page_count: 1, + total_count: 100, + duration_per_frame: 0, + frames_per_page_x: 10, + frames_per_page_y: 10, + ), + Frameset( + url_template: "https://i.ytimg.com/sb/pPvd8UxmSbQ/storyboard3_L1/M$M.jpg?sqp=-oaymwENSDfyq4qpAwVwAcABBqLzl_8DBgjf8LPxBQ==&sigh=rs$AOn4CLCWd3ylPF7ViQFBu5RUODMcusr_5g", + frame_width: 80, + frame_height: 45, + page_count: 1, + total_count: 83, + duration_per_frame: 2000, + frames_per_page_x: 10, + frames_per_page_y: 10, + ), + Frameset( + url_template: "https://i.ytimg.com/sb/pPvd8UxmSbQ/storyboard3_L2/M$M.jpg?sqp=-oaymwENSDfyq4qpAwVwAcABBqLzl_8DBgjf8LPxBQ==&sigh=rs$AOn4CLA6xat5cfw0e3EX_5SW-TPwkmExxA", + frame_width: 160, + frame_height: 90, + page_count: 4, + total_count: 83, + duration_per_frame: 2000, + frames_per_page_x: 5, + frames_per_page_y: 5, + ), + ], visitor_data: Some("Cgt2aHFtQU5YZFBvYyirsaWXBg%3D%3D"), ) diff --git a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_desktop.snap b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_desktop.snap index 56b35be..8de2d4a 100644 --- a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_desktop.snap +++ b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_desktop.snap @@ -569,5 +569,37 @@ VideoPlayer( expires_in_seconds: 21540, hls_manifest_url: None, dash_manifest_url: Some("https://manifest.googlevideo.com/api/manifest/dash/expire/1659481355/ei/q1jpYtq3BJCX1gKVyJGQDg/ip/2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e/id/a4fbddf14c6649b4/source/youtube/requiressl/yes/playback_host/rr4---sn-h0jelnez.googlevideo.com/mh/mQ/mm/31%2C26/mn/sn-h0jelnez%2Csn-4g5edn6k/ms/au%2Conr/mv/m/mvi/4/pl/37/hfr/all/as/fmp4_audio_clear%2Cwebm_audio_clear%2Cwebm2_audio_clear%2Cfmp4_sd_hd_clear%2Cwebm2_sd_hd_clear/initcwndbps/1513750/spc/lT-KhrZGE2opztWyVdAtyUNlb8dXPDs/vprv/1/mt/1659459429/fvip/4/keepalive/yes/fexp/24001373%2C24007246/itag/0/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cas%2Cspc%2Cvprv%2Citag/sig/AOq0QJ8wRgIhAPEjHK19PKVHqQeia6WF4qubuMYk74LGi8F8lk5ZMPkFAiEAsaB2pKQWBvuPnNUnbdQXHc-izgsHJUP793woC2xNJlg%3D/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgOY4xu4H9wqPVZ7vF2i0hFcOnqrur1XGoA43a7ZEuuSUCIQCyPxBKXUQrKFmknNEGpX5GSWySKgMw_xHBikWpKpKwvg%3D%3D"), + preview_frames: [ + Frameset( + url_template: "https://i.ytimg.com/sb/pPvd8UxmSbQ/storyboard3_L0/default.jpg?sqp=-oaymwENSDfyq4qpAwVwAcABBqLzl_8DBgjf8LPxBQ==&sigh=rs$AOn4CLAXobPyrylgm8IEvjlZzqYTiPe1Ow", + frame_width: 48, + frame_height: 27, + page_count: 1, + total_count: 100, + duration_per_frame: 0, + frames_per_page_x: 10, + frames_per_page_y: 10, + ), + Frameset( + url_template: "https://i.ytimg.com/sb/pPvd8UxmSbQ/storyboard3_L1/M$M.jpg?sqp=-oaymwENSDfyq4qpAwVwAcABBqLzl_8DBgjf8LPxBQ==&sigh=rs$AOn4CLCWd3ylPF7ViQFBu5RUODMcusr_5g", + frame_width: 80, + frame_height: 45, + page_count: 1, + total_count: 83, + duration_per_frame: 2000, + frames_per_page_x: 10, + frames_per_page_y: 10, + ), + Frameset( + url_template: "https://i.ytimg.com/sb/pPvd8UxmSbQ/storyboard3_L2/M$M.jpg?sqp=-oaymwENSDfyq4qpAwVwAcABBqLzl_8DBgjf8LPxBQ==&sigh=rs$AOn4CLA6xat5cfw0e3EX_5SW-TPwkmExxA", + frame_width: 160, + frame_height: 90, + page_count: 4, + total_count: 83, + duration_per_frame: 2000, + frames_per_page_x: 5, + frames_per_page_y: 5, + ), + ], visitor_data: Some("CgtoS1pCMVJTNUJISSirsaWXBg%3D%3D"), ) diff --git a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_desktopmusic.snap b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_desktopmusic.snap index 36f191a..19b2530 100644 --- a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_desktopmusic.snap +++ b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_desktopmusic.snap @@ -387,5 +387,37 @@ VideoPlayer( expires_in_seconds: 21540, hls_manifest_url: None, dash_manifest_url: Some("https://manifest.googlevideo.com/api/manifest/dash/expire/1659487474/ei/knDpYub6BojEgAf6jbLgDw/ip/2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e/id/a4fbddf14c6649b4/source/youtube/requiressl/yes/playback_host/rr5---sn-h0jeenek.googlevideo.com/mh/mQ/mm/31%2C29/mn/sn-h0jeenek%2Csn-h0jelnez/ms/au%2Crdu/mv/m/mvi/5/pl/37/hfr/all/as/fmp4_audio_clear%2Cwebm_audio_clear%2Cwebm2_audio_clear%2Cfmp4_sd_hd_clear%2Cwebm2_sd_hd_clear/initcwndbps/1418750/spc/lT-Khox4YuJQ2wmH79zYALRvsWTPCUc/vprv/1/mt/1659465669/fvip/4/keepalive/yes/fexp/24001373%2C24007246/itag/0/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cas%2Cspc%2Cvprv%2Citag/sig/AOq0QJ8wRAIgErABhAEaoKHUDu9dDbpxE_8gR4b8WWAi61fnu8UKnuICIEYrEKcHvqHdO4V3R7cvSGwi_HGH34IlQsKbziOfMBov/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgJxHmH0Sxo3cY_pW_ZzQ3hW9-7oz6K_pZWcUdrDDQ2sQCIQDJYNINQwLgKelgbO3CZYx7sMxdUAFpWdokmRBQ77vwvw%3D%3D"), + preview_frames: [ + Frameset( + url_template: "https://i.ytimg.com/sb/pPvd8UxmSbQ/storyboard3_L0/default.jpg?sqp=-oaymwENSDfyq4qpAwVwAcABBqLzl_8DBgjf8LPxBQ==&sigh=rs$AOn4CLAXobPyrylgm8IEvjlZzqYTiPe1Ow", + frame_width: 48, + frame_height: 27, + page_count: 1, + total_count: 100, + duration_per_frame: 0, + frames_per_page_x: 10, + frames_per_page_y: 10, + ), + Frameset( + url_template: "https://i.ytimg.com/sb/pPvd8UxmSbQ/storyboard3_L1/M$M.jpg?sqp=-oaymwENSDfyq4qpAwVwAcABBqLzl_8DBgjf8LPxBQ==&sigh=rs$AOn4CLCWd3ylPF7ViQFBu5RUODMcusr_5g", + frame_width: 80, + frame_height: 45, + page_count: 1, + total_count: 83, + duration_per_frame: 2000, + frames_per_page_x: 10, + frames_per_page_y: 10, + ), + Frameset( + url_template: "https://i.ytimg.com/sb/pPvd8UxmSbQ/storyboard3_L2/M$M.jpg?sqp=-oaymwENSDfyq4qpAwVwAcABBqLzl_8DBgjf8LPxBQ==&sigh=rs$AOn4CLA6xat5cfw0e3EX_5SW-TPwkmExxA", + frame_width: 160, + frame_height: 90, + page_count: 4, + total_count: 83, + duration_per_frame: 2000, + frames_per_page_x: 5, + frames_per_page_y: 5, + ), + ], visitor_data: Some("CgszSHZWNWs0SDhpTSiS4aWXBg%3D%3D"), ) diff --git a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_ios.snap b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_ios.snap index 771f676..c5b2870 100644 --- a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_ios.snap +++ b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_ios.snap @@ -168,5 +168,37 @@ VideoPlayer( expires_in_seconds: 21540, hls_manifest_url: Some("https://manifest.googlevideo.com/api/manifest/hls_variant/expire/1659481355/ei/q1jpYq-xHs7NgQev0bfwAQ/ip/2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e/id/a4fbddf14c6649b4/source/youtube/requiressl/yes/playback_host/rr4---sn-h0jelnez.googlevideo.com/mh/mQ/mm/31%2C29/mn/sn-h0jelnez%2Csn-h0jeenek/ms/au%2Crdu/mv/m/mvi/4/pl/37/hfr/1/demuxed/1/tts_caps/1/maudio/1/initcwndbps/1513750/vprv/1/go/1/mt/1659459429/fvip/5/nvgoi/1/short_key/1/ncsapi/1/keepalive/yes/fexp/24001373%2C24007246/dover/13/itag/0/playlist_type/DVR/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cdemuxed%2Ctts_caps%2Cmaudio%2Cvprv%2Cgo%2Citag%2Cplaylist_type/sig/AOq0QJ8wRQIhAIYnEHvIgJtJ8hehAXNtVY3qsgsq_GdOhWf2hkJZe6lCAiBxaRY_nubYp6hBizcAg_KFkKnkG-t2XYLRQ5wGdM3AjA%3D%3D/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRgIhAM_91Kk_0VLuSsR6nLCY7LdtWojyRAzXSScd_X9ShRROAiEA1AF4VY04F71NsAI8_j3iqjuXnWL9s6NoXHq7P8-bHx8%3D/file/index.m3u8"), dash_manifest_url: None, + preview_frames: [ + Frameset( + url_template: "https://i.ytimg.com/sb/pPvd8UxmSbQ/storyboard3_L0/default.jpg?sqp=-oaymwGbA0g48quKqQOSA4gBAZUBAAAEQpgBMqABPKgBBLABELABDbABDLABELABFbABH7ABJrABLbABDrABDrABD7ABErABF7ABK7ABLLABKbABD7ABDrABELABFbABH7ABKrABMrABKbABD7ABEbABFLABGLABJrABPbABOLABLbABEbABFLABHrABKrABMbABS7ABR7ABNrABFbABHLABKbABLrABObABR7ABTbABP7ABJbABLrABN7ABPbABR7ABUrABUbABRbABM7ABQLABQrABQ7ABTLABRLABRrABQ7gBEbgBEbgBFbgBI7gBRLgBQ7gBQ7gBQ7gBEbgBE7gBFrgBL7gBQ7gBQ7gBQ7gBQ7gBFbgBFrgBKbgBQ7gBQ7gBQ7gBQ7gBQ7gBI7gBL7gBQ7gBQ7gBQ7gBQ7gBQ7gBQ7gBRLgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQqLzl_8DBgjf8LPxBQ==&sigh=rs$AOn4CLCsCT8Lprh2S0ptmCRsWH7VtDl3YQ", + frame_width: 48, + frame_height: 27, + page_count: 1, + total_count: 100, + duration_per_frame: 0, + frames_per_page_x: 10, + frames_per_page_y: 10, + ), + Frameset( + url_template: "https://i.ytimg.com/sb/pPvd8UxmSbQ/storyboard3_L1/M$M.jpg?sqp=-oaymwGbA0g48quKqQOSA4gBAZUBAAAEQpgBMqABPKgBBLABELABDbABDLABELABFbABH7ABJrABLbABDrABDrABD7ABErABF7ABK7ABLLABKbABD7ABDrABELABFbABH7ABKrABMrABKbABD7ABEbABFLABGLABJrABPbABOLABLbABEbABFLABHrABKrABMbABS7ABR7ABNrABFbABHLABKbABLrABObABR7ABTbABP7ABJbABLrABN7ABPbABR7ABUrABUbABRbABM7ABQLABQrABQ7ABTLABRLABRrABQ7gBEbgBEbgBFbgBI7gBRLgBQ7gBQ7gBQ7gBEbgBE7gBFrgBL7gBQ7gBQ7gBQ7gBQ7gBFbgBFrgBKbgBQ7gBQ7gBQ7gBQ7gBQ7gBI7gBL7gBQ7gBQ7gBQ7gBQ7gBQ7gBQ7gBRLgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQqLzl_8DBgjf8LPxBQ==&sigh=rs$AOn4CLBXrdgfuYV1WLnTGXqZtSAUm8oZCA", + frame_width: 80, + frame_height: 45, + page_count: 1, + total_count: 83, + duration_per_frame: 2000, + frames_per_page_x: 10, + frames_per_page_y: 10, + ), + Frameset( + url_template: "https://i.ytimg.com/sb/pPvd8UxmSbQ/storyboard3_L2/M$M.jpg?sqp=-oaymwGbA0g48quKqQOSA4gBAZUBAAAEQpgBMqABPKgBBLABELABDbABDLABELABFbABH7ABJrABLbABDrABDrABD7ABErABF7ABK7ABLLABKbABD7ABDrABELABFbABH7ABKrABMrABKbABD7ABEbABFLABGLABJrABPbABOLABLbABEbABFLABHrABKrABMbABS7ABR7ABNrABFbABHLABKbABLrABObABR7ABTbABP7ABJbABLrABN7ABPbABR7ABUrABUbABRbABM7ABQLABQrABQ7ABTLABRLABRrABQ7gBEbgBEbgBFbgBI7gBRLgBQ7gBQ7gBQ7gBEbgBE7gBFrgBL7gBQ7gBQ7gBQ7gBQ7gBFbgBFrgBKbgBQ7gBQ7gBQ7gBQ7gBQ7gBI7gBL7gBQ7gBQ7gBQ7gBQ7gBQ7gBQ7gBRLgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQqLzl_8DBgjf8LPxBQ==&sigh=rs$AOn4CLCRazj84zMuwJLaCCc_PiUakX_YdQ", + frame_width: 160, + frame_height: 90, + page_count: 4, + total_count: 83, + duration_per_frame: 2000, + frames_per_page_x: 5, + frames_per_page_y: 5, + ), + ], visitor_data: Some("Cgs4TXV4dk13WVEyWSirsaWXBg%3D%3D"), ) diff --git a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_tvhtml5embed.snap b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_tvhtml5embed.snap index 5b7fad7..ab41d4a 100644 --- a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_tvhtml5embed.snap +++ b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_tvhtml5embed.snap @@ -569,5 +569,37 @@ VideoPlayer( expires_in_seconds: 21540, hls_manifest_url: None, dash_manifest_url: Some("https://manifest.googlevideo.com/api/manifest/dash/expire/1659481355/ei/q1jpYv-eJ9uF6dsPhvyH8As/ip/2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e/id/a4fbddf14c6649b4/source/youtube/requiressl/yes/playback_host/rr4---sn-h0jelnez.googlevideo.com/mh/mQ/mm/31%2C29/mn/sn-h0jelnez%2Csn-h0jeenek/ms/au%2Crdu/mv/m/mvi/4/pl/37/hfr/all/as/fmp4_audio_clear%2Cfmp4_sd_hd_clear/initcwndbps/1527500/vprv/1/mt/1659459429/fvip/5/keepalive/yes/fexp/24001373%2C24007246/itag/0/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cas%2Cvprv%2Citag/sig/AOq0QJ8wRQIhANKWS7GCN4pSoHIQ6BMZdOaHAD0I25nHwRj7ds4qrxdEAiBsd9l8WIceqF7-2xyR82DGecCiS9hgUIPJhdNhkwVpHg%3D%3D/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgMbu-wTOcXGCwGh27y0YZHktumKM1sopgxfQf8LCcCnECIQDnhFbgddOxwiQbnMOIcCn6ncpN54UyALRNigUSCp9Deg%3D%3D"), + preview_frames: [ + Frameset( + url_template: "https://i.ytimg.com/sb/pPvd8UxmSbQ/storyboard3_L0/default.jpg?sqp=-oaymwGbA0g48quKqQOSA4gBAZUBAAAEQpgBMqABPKgBBLABELABDbABDLABELABFbABH7ABJrABLbABDrABDrABD7ABErABF7ABK7ABLLABKbABD7ABDrABELABFbABH7ABKrABMrABKbABD7ABEbABFLABGLABJrABPbABOLABLbABEbABFLABHrABKrABMbABS7ABR7ABNrABFbABHLABKbABLrABObABR7ABTbABP7ABJbABLrABN7ABPbABR7ABUrABUbABRbABM7ABQLABQrABQ7ABTLABRLABRrABQ7gBEbgBEbgBFbgBI7gBRLgBQ7gBQ7gBQ7gBEbgBE7gBFrgBL7gBQ7gBQ7gBQ7gBQ7gBFbgBFrgBKbgBQ7gBQ7gBQ7gBQ7gBQ7gBI7gBL7gBQ7gBQ7gBQ7gBQ7gBQ7gBQ7gBRLgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQqLzl_8DBgjf8LPxBQ==&sigh=rs$AOn4CLCsCT8Lprh2S0ptmCRsWH7VtDl3YQ", + frame_width: 48, + frame_height: 27, + page_count: 1, + total_count: 100, + duration_per_frame: 0, + frames_per_page_x: 10, + frames_per_page_y: 10, + ), + Frameset( + url_template: "https://i.ytimg.com/sb/pPvd8UxmSbQ/storyboard3_L1/M$M.jpg?sqp=-oaymwGbA0g48quKqQOSA4gBAZUBAAAEQpgBMqABPKgBBLABELABDbABDLABELABFbABH7ABJrABLbABDrABDrABD7ABErABF7ABK7ABLLABKbABD7ABDrABELABFbABH7ABKrABMrABKbABD7ABEbABFLABGLABJrABPbABOLABLbABEbABFLABHrABKrABMbABS7ABR7ABNrABFbABHLABKbABLrABObABR7ABTbABP7ABJbABLrABN7ABPbABR7ABUrABUbABRbABM7ABQLABQrABQ7ABTLABRLABRrABQ7gBEbgBEbgBFbgBI7gBRLgBQ7gBQ7gBQ7gBEbgBE7gBFrgBL7gBQ7gBQ7gBQ7gBQ7gBFbgBFrgBKbgBQ7gBQ7gBQ7gBQ7gBQ7gBI7gBL7gBQ7gBQ7gBQ7gBQ7gBQ7gBQ7gBRLgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQqLzl_8DBgjf8LPxBQ==&sigh=rs$AOn4CLBXrdgfuYV1WLnTGXqZtSAUm8oZCA", + frame_width: 80, + frame_height: 45, + page_count: 1, + total_count: 83, + duration_per_frame: 2000, + frames_per_page_x: 10, + frames_per_page_y: 10, + ), + Frameset( + url_template: "https://i.ytimg.com/sb/pPvd8UxmSbQ/storyboard3_L2/M$M.jpg?sqp=-oaymwGbA0g48quKqQOSA4gBAZUBAAAEQpgBMqABPKgBBLABELABDbABDLABELABFbABH7ABJrABLbABDrABDrABD7ABErABF7ABK7ABLLABKbABD7ABDrABELABFbABH7ABKrABMrABKbABD7ABEbABFLABGLABJrABPbABOLABLbABEbABFLABHrABKrABMbABS7ABR7ABNrABFbABHLABKbABLrABObABR7ABTbABP7ABJbABLrABN7ABPbABR7ABUrABUbABRbABM7ABQLABQrABQ7ABTLABRLABRrABQ7gBEbgBEbgBFbgBI7gBRLgBQ7gBQ7gBQ7gBEbgBE7gBFrgBL7gBQ7gBQ7gBQ7gBQ7gBFbgBFrgBKbgBQ7gBQ7gBQ7gBQ7gBQ7gBI7gBL7gBQ7gBQ7gBQ7gBQ7gBQ7gBQ7gBRLgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQqLzl_8DBgjf8LPxBQ==&sigh=rs$AOn4CLCRazj84zMuwJLaCCc_PiUakX_YdQ", + frame_width: 160, + frame_height: 90, + page_count: 4, + total_count: 83, + duration_per_frame: 2000, + frames_per_page_x: 5, + frames_per_page_y: 5, + ), + ], visitor_data: Some("CgtacUJOMG81dTI3cyirsaWXBg%3D%3D"), ) diff --git a/src/model/frameset.rs b/src/model/frameset.rs new file mode 100644 index 0000000..cde1278 --- /dev/null +++ b/src/model/frameset.rs @@ -0,0 +1,61 @@ +use serde::{Deserialize, Serialize}; + +/// Set of video frames for seek preview +/// +/// YouTube generates a set of images containing a grid of frames for each video. +/// These images are used by the player for the seekbar preview. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[non_exhaustive] +pub struct Frameset { + /// Url template of the frameset + /// + /// The `$M` placeholder has to be replaced with the page index (starting from 0). + pub url_template: String, + /// Width of a single frame in pixels + pub frame_width: u32, + /// Height of a single frame in pixels + pub frame_height: u32, + /// Number of pages (individual images) + pub page_count: u32, + /// Total number of frames in the set + pub total_count: u32, + /// Duration per frame in milliseconds + pub duration_per_frame: u32, + /// Number of frames in the x direction + pub frames_per_page_x: u32, + /// Number of frames in the y direction. + pub frames_per_page_y: u32, +} + +/// Iterator producing frameset page urls +pub struct FramesetUrls<'a> { + frameset: &'a Frameset, + i: u32, +} + +impl Frameset { + /// Gets an iterator over the page URLs of the frameset + pub fn urls(&self) -> FramesetUrls { + FramesetUrls { + frameset: self, + i: 0, + } + } +} + +impl Iterator for FramesetUrls<'_> { + type Item = String; + + fn next(&mut self) -> Option { + if self.i < self.frameset.page_count { + let url = self + .frameset + .url_template + .replace("$M", &self.i.to_string()); + self.i += 1; + Some(url) + } else { + None + } + } +} diff --git a/src/model/mod.rs b/src/model/mod.rs index 8b73556..0250aaa 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -1,23 +1,22 @@ //! YouTube API response models mod convert; +mod frameset; mod ordering; pub mod paginator; pub mod richtext; - pub mod traits; - -use serde_with::serde_as; +pub use frameset::{Frameset, FramesetUrls}; use std::{collections::BTreeSet, ops::Range}; use serde::{Deserialize, Serialize}; +use serde_with::serde_as; use time::{Date, OffsetDateTime}; -use crate::{error::Error, param::Country, serializer::DateYmd, util}; - use self::{paginator::Paginator, richtext::RichText}; +use crate::{error::Error, param::Country, serializer::DateYmd, util}; /* #COMMON @@ -155,6 +154,8 @@ pub struct VideoPlayer { pub hls_manifest_url: Option, /// Dash manifest URL (for livestreams) pub dash_manifest_url: Option, + /// Video frames for seek preview + pub preview_frames: Vec, /// YouTube visitor data cookie pub visitor_data: Option, } diff --git a/testfiles/player_model/hdr.json b/testfiles/player_model/hdr.json index 4a9d8c1..d9b1f83 100644 --- a/testfiles/player_model/hdr.json +++ b/testfiles/player_model/hdr.json @@ -1124,5 +1124,6 @@ "subtitles": [], "expires_in_seconds": 21540, "hls_manifest_url": null, - "dash_manifest_url": null + "dash_manifest_url": null, + "preview_frames": [] } diff --git a/testfiles/player_model/multilanguage.json b/testfiles/player_model/multilanguage.json index ba8bde8..f1cb210 100644 --- a/testfiles/player_model/multilanguage.json +++ b/testfiles/player_model/multilanguage.json @@ -2119,5 +2119,6 @@ "expires_in_seconds": 21540, "hls_manifest_url": null, "dash_manifest_url": null, + "preview_frames": [], "visitor_data": "CgtGWDFCUllrcTdxayjo1_OiBg%3D%3D" } diff --git a/tests/youtube.rs b/tests/youtube.rs index 8d71802..7ff394b 100644 --- a/tests/youtube.rs +++ b/tests/youtube.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use std::fmt::Display; use std::str::FromStr; +use reqwest::Client; use rstest::{fixture, rstest}; use rustypipe::model::paginator::ContinuationEndpoint; use rustypipe::param::{ChannelOrder, ChannelVideoTab, Language}; @@ -15,8 +16,8 @@ use rustypipe::model::{ paginator::Paginator, richtext::ToPlaintext, traits::{FromYtItem, YtStream}, - AlbumType, AudioCodec, AudioFormat, AudioTrackType, Channel, MusicGenre, MusicItemType, - UrlTarget, Verification, VideoCodec, VideoFormat, YouTubeItem, + AlbumType, AudioCodec, AudioFormat, AudioTrackType, Channel, Frameset, MusicGenre, + MusicItemType, UrlTarget, Verification, VideoCodec, VideoFormat, YouTubeItem, }; use rustypipe::param::{ search_filter::{self, SearchFilter}, @@ -287,6 +288,11 @@ fn get_player( }; assert_gte(player_data.expires_in_seconds, 10_000, "expiry time"); + + if !is_live { + assert_gte(player_data.preview_frames.len(), 3, "preview framesets"); + player_data.preview_frames.iter().for_each(assert_frameset); + } } #[rstest] @@ -2375,3 +2381,37 @@ fn assert_album_id(id: &str) { fn assert_playlist_id(id: &str) { assert!(validate::playlist_id(id), "invalid playlist id: `{id}`"); } + +// fn assert_image(client: &Client, url: &str) { +// let resp = tokio_test::block_on(client.get(url).send()) +// .unwrap() +// .error_for_status() +// .unwrap(); +// let ctype = resp +// .headers() +// .get(reqwest::header::CONTENT_TYPE) +// .unwrap() +// .to_str() +// .unwrap(); + +// assert!(ctype.starts_with("image/"), "content type: {ctype}"); +// } + +fn assert_frameset(frameset: &Frameset) { + assert_gte(frameset.frame_height, 20, "frame height"); + assert_gte(frameset.frame_height, 20, "frame width"); + assert_gte(frameset.page_count, 1, "page count"); + assert_gte(frameset.total_count, 50, "total count"); + assert_gte(frameset.frames_per_page_x, 5, "frames per page x"); + assert_gte(frameset.frames_per_page_y, 5, "frames per page y"); + + // let client = Client::new(); + + let n = frameset + .urls() + // .map(|url| { + // assert_image(&client, &url); + // }) + .count() as u32; + assert_eq!(n, frameset.page_count); +}