feat: redirect secondary YT channels to the YTM channel

Squashed commit of the following:

commit 88809265ead6eadfafab4b74091dd1af357b9577
Author: ThetaDev <t.testboy@gmail.com>
Date:   Sat Jan 21 22:16:23 2023 +0100

    feat: redirect secondary YT channels to the YTM channel

commit 02cc120912509f40f45da243ba5d37798b9ff411
Author: ThetaDev <t.testboy@gmail.com>
Date:   Mon Jan 9 23:57:18 2023 +0100

    add artists_no_tracks testfile
This commit is contained in:
ThetaDev 2023-01-21 22:18:25 +01:00
parent f44bc6434a
commit a706a7011b
11 changed files with 13543 additions and 23 deletions

View file

@ -34,6 +34,20 @@ impl RustyPipeQuery {
artist_id: S,
all_albums: bool,
) -> Result<MusicArtist, Error> {
let res = self._music_artist(artist_id, all_albums).await;
if let Err(Error::Extraction(ExtractionError::Redirect(id))) = res {
self._music_artist(&id, all_albums).await.map(|x| *x)
} else {
res.map(|x| *x)
}
}
async fn _music_artist<S: AsRef<str>>(
&self,
artist_id: S,
all_albums: bool,
) -> Result<Box<MusicArtist>, Error> {
let artist_id = artist_id.as_ref();
if all_albums {
@ -74,7 +88,7 @@ impl RustyPipeQuery {
artist.albums.append(&mut res);
}
Ok(artist)
Ok(artist.into())
} else {
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
let request_body = QBrowse {
@ -90,6 +104,7 @@ impl RustyPipeQuery {
&request_body,
)
.await
.map(|x: MusicArtist| x.into())
}
}
@ -155,6 +170,21 @@ fn map_artist_page(
let header = res.header.music_immersive_header_renderer;
if let Some(share) = header.share_endpoint {
let pb = share.share_entity_endpoint.serialized_share_entity;
let share_channel_id = urlencoding::decode(&pb)
.ok()
.and_then(|pb| base64::decode(pb.as_bytes()).ok())
.and_then(|pb| util::string_from_pb(pb, 3));
if let Some(share_channel_id) = share_channel_id {
if share_channel_id != id {
return Err(ExtractionError::Redirect(share_channel_id));
}
}
}
let mut content = res.contents.single_column_browse_results_renderer.contents;
let sections = content
.try_swap_remove(0)
@ -390,4 +420,23 @@ mod tests {
);
insta::assert_ron_snapshot!(map_res.c);
}
#[test]
fn map_music_artist_secondary_channel() {
let json_path = path!("testfiles" / "music_artist" / "artist_secondary_channel.json");
let json_file = File::open(json_path).unwrap();
let artist: response::MusicArtist =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
let res: Result<MapResult<MusicArtist>, ExtractionError> =
artist.map_response("UCLkAepWjdylmXSltofFvsYQ", Language::En, None);
let e = res.unwrap_err();
match e {
ExtractionError::Redirect(id) => {
assert_eq!(id, "UCOR4_bSVIXPsGa4BbCSt60Q")
}
_ => panic!("error: {}", e),
}
}
}

View file

@ -61,8 +61,7 @@ impl RustyPipeQuery {
/// Get YouTube player data (video/audio streams + basic metadata)
pub async fn player<S: AsRef<str>>(&self, video_id: S) -> Result<VideoPlayer, Error> {
let video_id = video_id.as_ref();
let q1 = self.clone();
let android_res = q1.player_from_client(video_id, ClientType::Android).await;
let android_res = self.player_from_client(video_id, ClientType::Android).await;
match android_res {
Ok(res) => Ok(res),

View file

@ -38,6 +38,9 @@ pub(crate) struct MusicHeaderRenderer {
pub description: Option<String>,
#[serde(default)]
pub thumbnail: MusicThumbnailRenderer,
#[serde(default)]
#[serde_as(as = "DefaultOnError")]
pub share_endpoint: Option<ShareEndpoint>,
}
#[derive(Debug, Deserialize)]
@ -54,6 +57,18 @@ pub(crate) struct SubscriptionButtonRenderer {
pub subscriber_count_text: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ShareEndpoint {
pub share_entity_endpoint: ShareEntityEndpoint,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ShareEntityEndpoint {
pub serialized_share_entity: String,
}
/// Response model for YouTube Music artist album page
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]