fix: A/B test 22: commandExecutorCommand for playlist continuations
This commit is contained in:
parent
fcf27aa3b2
commit
e8acbfbbcf
12 changed files with 33753 additions and 36 deletions
|
|
@ -41,10 +41,15 @@ pub enum ABTest {
|
|||
MusicAlbumGroupsReordered = 19,
|
||||
MusicContinuationItemRenderer = 20,
|
||||
AlbumRecommends = 21,
|
||||
CommandExecutorCommand = 22,
|
||||
}
|
||||
|
||||
/// List of active A/B tests that are run when none is manually specified
|
||||
const TESTS_TO_RUN: &[ABTest] = &[ABTest::MusicAlbumGroupsReordered, ABTest::AlbumRecommends];
|
||||
const TESTS_TO_RUN: &[ABTest] = &[
|
||||
ABTest::MusicAlbumGroupsReordered,
|
||||
ABTest::AlbumRecommends,
|
||||
ABTest::CommandExecutorCommand,
|
||||
];
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ABTestRes {
|
||||
|
|
@ -120,6 +125,7 @@ pub async fn run_test(
|
|||
music_continuation_item_renderer(&query).await
|
||||
}
|
||||
ABTest::AlbumRecommends => album_recommends(&query).await,
|
||||
ABTest::CommandExecutorCommand => command_executor_command(&query).await,
|
||||
}
|
||||
.unwrap();
|
||||
pb.inc(1);
|
||||
|
|
@ -457,3 +463,18 @@ pub async fn album_recommends(rp: &RustyPipeQuery) -> Result<bool> {
|
|||
.await?;
|
||||
Ok(res.contains("\"musicCarouselShelfRenderer\""))
|
||||
}
|
||||
|
||||
pub async fn command_executor_command(rp: &RustyPipeQuery) -> Result<bool> {
|
||||
let id = "VLPLbZIPy20-1pN7mqjckepWF78ndb6ci_qi";
|
||||
let res = rp
|
||||
.raw(
|
||||
ClientType::Desktop,
|
||||
"browse",
|
||||
&QBrowse {
|
||||
browse_id: id,
|
||||
params: None,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(res.contains("\"commandExecutorCommand\""))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1030,7 +1030,7 @@ commandContext missing).
|
|||
- **Encountered on:** 13.01.2025
|
||||
- **Impact:** 🟢 Low
|
||||
- **Endpoint:** browse (YTM)
|
||||
- **Status:** Common (10%)
|
||||
- **Status:** Frequent (59%)
|
||||
|
||||
YouTube Music used to group artist albums into 2 rows: "Albums" and "Singles".
|
||||
|
||||
|
|
@ -1067,3 +1067,37 @@ pages. The difficulty is distinguishing them reliably for parsing the album vari
|
|||
|
||||
The current solution is adding the "Other versions" title in all languages to the
|
||||
dictionary and comparing it.
|
||||
|
||||
## [22] commandExecutorCommand for continuations
|
||||
|
||||
- **Encountered on:** 16.03.2025
|
||||
- **Impact:** 🟢 Low
|
||||
- **Endpoint:** browse (YTM)
|
||||
- **Status:** Experimental (1%)
|
||||
|
||||
YouTube playlists may use a commandExecutorCommand which holds a list of commands: the
|
||||
`continuationCommand` that needs to be extracted as well as a `playlistVotingRefreshPopupCommand`.
|
||||
|
||||
```json
|
||||
{
|
||||
"continuationItemRenderer": {
|
||||
"continuationEndpoint": {
|
||||
"commandExecutorCommand": {
|
||||
"commands": [
|
||||
{
|
||||
"playlistVotingRefreshPopupCommand": {
|
||||
"command": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"continuationCommand": {
|
||||
"request": "CONTINUATION_REQUEST_TYPE_BROWSE",
|
||||
"token": "4qmFsgKBARIkVkxQTGJaSVB5MjAtMXBON21xamNrZXBXRjc4bmRiNmNpX3FpGjRDQUY2SGxCVU9rTklTV2xGUkVreVVtdEZOVTVFU1hsU2FrWkRVa1JKZWs1NldRJTNEJTNEmgIiUExiWklQeTIwLTFwTjdtcWpja2VwV0Y3OG5kYjZjaV9xaQ%3D%3D"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -249,11 +249,9 @@ impl MapResponse<Paginator<HistoryItem<VideoItem>>> for response::Continuation {
|
|||
&mut map_res,
|
||||
);
|
||||
}
|
||||
response::YouTubeListItem::ContinuationItemRenderer {
|
||||
continuation_endpoint,
|
||||
} => {
|
||||
response::YouTubeListItem::ContinuationItemRenderer(ep) => {
|
||||
if ctoken.is_none() {
|
||||
ctoken = Some(continuation_endpoint.continuation_command.token);
|
||||
ctoken = ep.continuation_endpoint.into_token();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
|
|
|||
|
|
@ -257,6 +257,7 @@ mod tests {
|
|||
#[case::nomusic("nomusic", "PL1J-6JOckZtE_P9Xx8D3b2O6w0idhuKBe")]
|
||||
#[case::live("live", "UULVvqRdlKsE5Q8mf8YXbdIJLw")]
|
||||
#[case::pageheader("20241011_pageheader", "PLT2w2oBf1TZKyvY_M6JsASs73m-wjLzH5")]
|
||||
#[case::cmdexecutor("20250316_cmdexecutor", "PLbZIPy20-1pN7mqjckepWF78ndb6ci_qi")]
|
||||
fn map_playlist_data(#[case] name: &str, #[case] id: &str) {
|
||||
let json_path = path!(*TESTFILES / "playlist" / format!("playlist_{name}.json"));
|
||||
let json_file = File::open(json_path).unwrap();
|
||||
|
|
|
|||
|
|
@ -152,9 +152,16 @@ pub(crate) struct ContinuationItemRenderer {
|
|||
pub continuation_endpoint: ContinuationEndpoint,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub(crate) enum ContinuationEndpoint {
|
||||
ContinuationCommand(ContinuationCommandWrap),
|
||||
CommandExecutorCommand(CommandExecutorCommandWrap),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ContinuationEndpoint {
|
||||
pub(crate) struct ContinuationCommandWrap {
|
||||
pub continuation_command: ContinuationCommand,
|
||||
}
|
||||
|
||||
|
|
@ -164,7 +171,34 @@ pub(crate) struct ContinuationCommand {
|
|||
pub token: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct CommandExecutorCommandWrap {
|
||||
pub command_executor_command: CommandExecutorCommand,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct CommandExecutorCommand {
|
||||
#[serde_as(as = "VecSkipError<_>")]
|
||||
commands: Vec<ContinuationCommandWrap>,
|
||||
}
|
||||
|
||||
impl ContinuationEndpoint {
|
||||
pub fn into_token(self) -> Option<String> {
|
||||
match self {
|
||||
Self::ContinuationCommand(cmd) => Some(cmd.continuation_command.token),
|
||||
Self::CommandExecutorCommand(cmd) => cmd
|
||||
.command_executor_command
|
||||
.commands
|
||||
.into_iter()
|
||||
.next()
|
||||
.map(|c| c.continuation_command.token),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct Icon {
|
||||
|
|
|
|||
|
|
@ -530,7 +530,9 @@ impl MusicListMapper {
|
|||
MusicResponseItem::ContinuationItemRenderer {
|
||||
continuation_endpoint,
|
||||
} => {
|
||||
self.ctoken = Some(continuation_endpoint.continuation_command.token);
|
||||
if self.ctoken.is_none() {
|
||||
self.ctoken = continuation_endpoint.into_token();
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -530,15 +530,14 @@ pub(crate) enum ContinuationItemVariants {
|
|||
}
|
||||
|
||||
impl ContinuationItemVariants {
|
||||
pub fn token(self) -> String {
|
||||
pub fn into_token(self) -> Option<String> {
|
||||
match self {
|
||||
ContinuationItemVariants::Ep {
|
||||
continuation_endpoint,
|
||||
} => continuation_endpoint,
|
||||
ContinuationItemVariants::Btn { button } => button.button_renderer.command,
|
||||
}
|
||||
.continuation_command
|
||||
.token
|
||||
.into_token()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use serde_with::{
|
|||
};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use super::{ChannelBadge, ContentImage, ContinuationEndpoint, PhMetadataView, Thumbnails};
|
||||
use super::{ChannelBadge, ContentImage, ContinuationItemRenderer, PhMetadataView, Thumbnails};
|
||||
use crate::{
|
||||
model::{Channel, ChannelItem, ChannelTag, PlaylistItem, VideoItem, YouTubeItem},
|
||||
param::Language,
|
||||
|
|
@ -37,12 +37,9 @@ pub(crate) enum YouTubeListItem {
|
|||
|
||||
LockupViewModel(LockupViewModel),
|
||||
|
||||
/// Continauation items are located at the end of a list
|
||||
/// Continuation items are located at the end of a list
|
||||
/// and contain the continuation token for progressive loading
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ContinuationItemRenderer {
|
||||
continuation_endpoint: ContinuationEndpoint,
|
||||
},
|
||||
ContinuationItemRenderer(ContinuationItemRenderer),
|
||||
|
||||
/// Corrected search query
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
|
@ -838,9 +835,11 @@ impl YouTubeListMapper<YouTubeItem> {
|
|||
self.items.push(mapped);
|
||||
}
|
||||
}
|
||||
YouTubeListItem::ContinuationItemRenderer {
|
||||
continuation_endpoint,
|
||||
} => self.ctoken = Some(continuation_endpoint.continuation_command.token),
|
||||
YouTubeListItem::ContinuationItemRenderer(r) => {
|
||||
if self.ctoken.is_none() {
|
||||
self.ctoken = r.continuation_endpoint.into_token();
|
||||
}
|
||||
}
|
||||
YouTubeListItem::ShowingResultsForRenderer { corrected_query } => {
|
||||
self.corrected_query = Some(corrected_query);
|
||||
}
|
||||
|
|
@ -886,9 +885,11 @@ impl YouTubeListMapper<VideoItem> {
|
|||
self.items.push(mapped);
|
||||
}
|
||||
}
|
||||
YouTubeListItem::ContinuationItemRenderer {
|
||||
continuation_endpoint,
|
||||
} => self.ctoken = Some(continuation_endpoint.continuation_command.token),
|
||||
YouTubeListItem::ContinuationItemRenderer(r) => {
|
||||
if self.ctoken.is_none() {
|
||||
self.ctoken = r.continuation_endpoint.into_token();
|
||||
}
|
||||
}
|
||||
YouTubeListItem::ShowingResultsForRenderer { corrected_query } => {
|
||||
self.corrected_query = Some(corrected_query);
|
||||
}
|
||||
|
|
@ -938,9 +939,11 @@ impl YouTubeListMapper<PlaylistItem> {
|
|||
self.items.push(mapped);
|
||||
}
|
||||
}
|
||||
YouTubeListItem::ContinuationItemRenderer {
|
||||
continuation_endpoint,
|
||||
} => self.ctoken = Some(continuation_endpoint.continuation_command.token),
|
||||
YouTubeListItem::ContinuationItemRenderer(r) => {
|
||||
if self.ctoken.is_none() {
|
||||
self.ctoken = r.continuation_endpoint.into_token();
|
||||
}
|
||||
}
|
||||
YouTubeListItem::ShowingResultsForRenderer { corrected_query } => {
|
||||
self.corrected_query = Some(corrected_query);
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -207,11 +207,9 @@ impl MapResponse<Paginator<HistoryItem<VideoItem>>> for response::History {
|
|||
&mut map_res,
|
||||
);
|
||||
}
|
||||
response::YouTubeListItem::ContinuationItemRenderer {
|
||||
continuation_endpoint,
|
||||
} => {
|
||||
response::YouTubeListItem::ContinuationItemRenderer(ep) => {
|
||||
if ctoken.is_none() {
|
||||
ctoken = Some(continuation_endpoint.continuation_command.token);
|
||||
ctoken = ep.continuation_endpoint.into_token();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
|
|
|||
|
|
@ -208,11 +208,10 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
|
|||
)
|
||||
});
|
||||
|
||||
let comment_ctoken = comment_ctoken_section.map(|s| {
|
||||
let comment_ctoken = comment_ctoken_section.and_then(|s| {
|
||||
s.continuation_item_renderer
|
||||
.continuation_endpoint
|
||||
.continuation_command
|
||||
.token
|
||||
.into_token()
|
||||
});
|
||||
|
||||
let (owner, description, is_ccommons) = match secondary_info {
|
||||
|
|
@ -333,7 +332,7 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
|
|||
.sub_menu_items;
|
||||
items
|
||||
.try_swap_remove(1)
|
||||
.map(|c| c.service_endpoint.continuation_command.token)
|
||||
.and_then(|c| c.service_endpoint.into_token())
|
||||
});
|
||||
|
||||
Ok(MapResult {
|
||||
|
|
@ -453,7 +452,9 @@ impl MapResponse<Paginator<Comment>> for response::VideoComments {
|
|||
}
|
||||
}
|
||||
response::video_details::CommentListItem::ContinuationItemRenderer(cont) => {
|
||||
ctoken = Some(cont.token());
|
||||
if ctoken.is_none() {
|
||||
ctoken = cont.into_token();
|
||||
}
|
||||
}
|
||||
response::video_details::CommentListItem::CommentsHeaderRenderer { count_text } => {
|
||||
comment_count = count_text
|
||||
|
|
@ -520,7 +521,9 @@ fn map_replies(
|
|||
))
|
||||
}
|
||||
response::video_details::CommentListItem::ContinuationItemRenderer(cont) => {
|
||||
reply_ctoken = Some(cont.token());
|
||||
if reply_ctoken.is_none() {
|
||||
reply_ctoken = cont.into_token();
|
||||
}
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
|
|
|
|||
29478
testfiles/playlist/playlist_20250316_cmdexecutor.json
Normal file
29478
testfiles/playlist/playlist_20250316_cmdexecutor.json
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue