Compare commits
	
		
			4 commits
		
	
	
		
			
				38bc12f695
			
			...
			
				2b91c76b85
			
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 2b91c76b85 | |||
| 3eadf82b8b | |||
| a9aeb4dabe | |||
| 01a2717c11 | 
					 14 changed files with 160 additions and 39 deletions
				
			
		| 
						 | 
					@ -99,7 +99,10 @@ impl RustyPipeQuery {
 | 
				
			||||||
        radio_id: S,
 | 
					        radio_id: S,
 | 
				
			||||||
    ) -> Result<Paginator<TrackItem>, Error> {
 | 
					    ) -> Result<Paginator<TrackItem>, Error> {
 | 
				
			||||||
        let radio_id = radio_id.as_ref();
 | 
					        let radio_id = radio_id.as_ref();
 | 
				
			||||||
        let context = self.get_context(ClientType::DesktopMusic, true, None).await;
 | 
					        let visitor_data = self.get_ytm_visitor_data().await?;
 | 
				
			||||||
 | 
					        let context = self
 | 
				
			||||||
 | 
					            .get_context(ClientType::DesktopMusic, true, Some(&visitor_data))
 | 
				
			||||||
 | 
					            .await;
 | 
				
			||||||
        let request_body = QRadio {
 | 
					        let request_body = QRadio {
 | 
				
			||||||
            context,
 | 
					            context,
 | 
				
			||||||
            playlist_id: radio_id,
 | 
					            playlist_id: radio_id,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -45,14 +45,55 @@ impl RustyPipeQuery {
 | 
				
			||||||
            browse_id: album_id,
 | 
					            browse_id: album_id,
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.execute_request::<response::MusicPlaylist, _, _>(
 | 
					        let mut album = self
 | 
				
			||||||
 | 
					            .execute_request::<response::MusicPlaylist, MusicAlbum, _>(
 | 
				
			||||||
                ClientType::DesktopMusic,
 | 
					                ClientType::DesktopMusic,
 | 
				
			||||||
                "music_album",
 | 
					                "music_album",
 | 
				
			||||||
                album_id,
 | 
					                album_id,
 | 
				
			||||||
                "browse",
 | 
					                "browse",
 | 
				
			||||||
                &request_body,
 | 
					                &request_body,
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
        .await
 | 
					            .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // YouTube Music is replacing album tracks with their respective music videos. To get the original
 | 
				
			||||||
 | 
					        // tracks, we have to fetch the album as a playlist and replace the offending track ids.
 | 
				
			||||||
 | 
					        if let Some(playlist_id) = &album.playlist_id {
 | 
				
			||||||
 | 
					            // Get a list of music videos in the album
 | 
				
			||||||
 | 
					            let to_replace = album
 | 
				
			||||||
 | 
					                .tracks
 | 
				
			||||||
 | 
					                .iter()
 | 
				
			||||||
 | 
					                .enumerate()
 | 
				
			||||||
 | 
					                .filter_map(|(i, track)| {
 | 
				
			||||||
 | 
					                    if track.is_video {
 | 
				
			||||||
 | 
					                        Some((i, track.title.to_owned()))
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        None
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					                .collect::<Vec<_>>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if !to_replace.is_empty() {
 | 
				
			||||||
 | 
					                let playlist = self.music_playlist(playlist_id).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                for (i, title) in to_replace {
 | 
				
			||||||
 | 
					                    let found_track = playlist.tracks.items.iter().find_map(|track| {
 | 
				
			||||||
 | 
					                        if track.title == title && !track.is_video {
 | 
				
			||||||
 | 
					                            Some((track.id.to_owned(), track.duration))
 | 
				
			||||||
 | 
					                        } else {
 | 
				
			||||||
 | 
					                            None
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                    if let Some((track_id, duration)) = found_track {
 | 
				
			||||||
 | 
					                        album.tracks[i].id = track_id;
 | 
				
			||||||
 | 
					                        if let Some(duration) = duration {
 | 
				
			||||||
 | 
					                            album.tracks[i].duration = Some(duration);
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        album.tracks[i].is_video = false;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        Ok(album)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -418,10 +418,12 @@ impl MusicListMapper {
 | 
				
			||||||
            // List item
 | 
					            // List item
 | 
				
			||||||
            MusicResponseItem::MusicResponsiveListItemRenderer(item) => {
 | 
					            MusicResponseItem::MusicResponsiveListItemRenderer(item) => {
 | 
				
			||||||
                let mut columns = item.flex_columns.into_iter();
 | 
					                let mut columns = item.flex_columns.into_iter();
 | 
				
			||||||
                let title = columns.next().map(|col| col.renderer.text.to_string());
 | 
					                let c1 = columns.next();
 | 
				
			||||||
                let c2 = columns.next();
 | 
					                let c2 = columns.next();
 | 
				
			||||||
                let c3 = columns.next();
 | 
					                let c3 = columns.next();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                let title = c1.as_ref().map(|col| col.renderer.text.to_string());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                let first_tn = item
 | 
					                let first_tn = item
 | 
				
			||||||
                    .thumbnail
 | 
					                    .thumbnail
 | 
				
			||||||
                    .music_thumbnail_renderer
 | 
					                    .music_thumbnail_renderer
 | 
				
			||||||
| 
						 | 
					@ -433,27 +435,54 @@ impl MusicListMapper {
 | 
				
			||||||
                    .navigation_endpoint
 | 
					                    .navigation_endpoint
 | 
				
			||||||
                    .and_then(|ne| ne.music_page())
 | 
					                    .and_then(|ne| ne.music_page())
 | 
				
			||||||
                    .or_else(|| {
 | 
					                    .or_else(|| {
 | 
				
			||||||
                        item.playlist_item_data
 | 
					                        c1.and_then(|c1| {
 | 
				
			||||||
                            .map(|d| (MusicPageType::Track, d.video_id))
 | 
					                            c1.renderer.text.0.into_iter().next().and_then(|t| match t {
 | 
				
			||||||
 | 
					                                crate::serializer::text::TextComponent::Video {
 | 
				
			||||||
 | 
					                                    video_id,
 | 
				
			||||||
 | 
					                                    is_video,
 | 
				
			||||||
 | 
					                                    ..
 | 
				
			||||||
 | 
					                                } => Some((MusicPageType::Track { is_video }, video_id)),
 | 
				
			||||||
 | 
					                                crate::serializer::text::TextComponent::Browse {
 | 
				
			||||||
 | 
					                                    page_type,
 | 
				
			||||||
 | 
					                                    browse_id,
 | 
				
			||||||
 | 
					                                    ..
 | 
				
			||||||
 | 
					                                } => Some((page_type.into(), browse_id)),
 | 
				
			||||||
 | 
					                                _ => None,
 | 
				
			||||||
 | 
					                            })
 | 
				
			||||||
 | 
					                        })
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					                    .or_else(|| {
 | 
				
			||||||
 | 
					                        item.playlist_item_data.map(|d| {
 | 
				
			||||||
 | 
					                            (
 | 
				
			||||||
 | 
					                                MusicPageType::Track {
 | 
				
			||||||
 | 
					                                    is_video: self.album.is_none()
 | 
				
			||||||
 | 
					                                        && !first_tn
 | 
				
			||||||
 | 
					                                            .map(|tn| tn.height == tn.width)
 | 
				
			||||||
 | 
					                                            .unwrap_or_default(),
 | 
				
			||||||
 | 
					                                },
 | 
				
			||||||
 | 
					                                d.video_id,
 | 
				
			||||||
 | 
					                            )
 | 
				
			||||||
 | 
					                        })
 | 
				
			||||||
                    })
 | 
					                    })
 | 
				
			||||||
                    .or_else(|| {
 | 
					                    .or_else(|| {
 | 
				
			||||||
                        first_tn.and_then(|tn| {
 | 
					                        first_tn.and_then(|tn| {
 | 
				
			||||||
                            util::video_id_from_thumbnail_url(&tn.url)
 | 
					                            util::video_id_from_thumbnail_url(&tn.url).map(|id| {
 | 
				
			||||||
                                .map(|id| (MusicPageType::Track, id))
 | 
					                                (
 | 
				
			||||||
 | 
					                                    MusicPageType::Track {
 | 
				
			||||||
 | 
					                                        is_video: self.album.is_none() && tn.width != tn.height,
 | 
				
			||||||
 | 
					                                    },
 | 
				
			||||||
 | 
					                                    id,
 | 
				
			||||||
 | 
					                                )
 | 
				
			||||||
 | 
					                            })
 | 
				
			||||||
                        })
 | 
					                        })
 | 
				
			||||||
                    });
 | 
					                    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                match pt_id {
 | 
					                match pt_id {
 | 
				
			||||||
                    // Track
 | 
					                    // Track
 | 
				
			||||||
                    Some((MusicPageType::Track, id)) => {
 | 
					                    Some((MusicPageType::Track { is_video }, id)) => {
 | 
				
			||||||
                        let title =
 | 
					                        let title =
 | 
				
			||||||
                            title.ok_or_else(|| format!("track {}: could not get title", id))?;
 | 
					                            title.ok_or_else(|| format!("track {}: could not get title", id))?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        // Videos have rectangular thumbnails, YTM tracks have square covers
 | 
					 | 
				
			||||||
                        // Exception: there are no thumbnails on album items
 | 
					 | 
				
			||||||
                        let is_video = self.album.is_none()
 | 
					 | 
				
			||||||
                            && !first_tn.map(|tn| tn.height == tn.width).unwrap_or_default();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        let (artists_p, album_p, duration_p) = match item.flex_column_display_style
 | 
					                        let (artists_p, album_p, duration_p) = match item.flex_column_display_style
 | 
				
			||||||
                        {
 | 
					                        {
 | 
				
			||||||
                            // Search result
 | 
					                            // Search result
 | 
				
			||||||
| 
						 | 
					@ -519,15 +548,14 @@ impl MusicListMapper {
 | 
				
			||||||
                                }),
 | 
					                                }),
 | 
				
			||||||
                            ),
 | 
					                            ),
 | 
				
			||||||
                            (_, false) => (
 | 
					                            (_, false) => (
 | 
				
			||||||
                                album_p
 | 
					                                album_p.and_then(|p| {
 | 
				
			||||||
                                    .and_then(|p| {
 | 
					 | 
				
			||||||
                                    p.0.into_iter().find_map(|c| AlbumId::try_from(c).ok())
 | 
					                                    p.0.into_iter().find_map(|c| AlbumId::try_from(c).ok())
 | 
				
			||||||
                                    })
 | 
					                                }),
 | 
				
			||||||
                                    .or_else(|| self.album.clone()),
 | 
					 | 
				
			||||||
                                None,
 | 
					                                None,
 | 
				
			||||||
                            ),
 | 
					                            ),
 | 
				
			||||||
                            (FlexColumnDisplayStyle::Default, true) => (None, None),
 | 
					                            (FlexColumnDisplayStyle::Default, true) => (None, None),
 | 
				
			||||||
                        };
 | 
					                        };
 | 
				
			||||||
 | 
					                        let album = album.or_else(|| self.album.clone());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        let (mut artists, _) = map_artists(artists_p);
 | 
					                        let (mut artists, _) = map_artists(artists_p);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -640,7 +668,8 @@ impl MusicListMapper {
 | 
				
			||||||
                                // There may be broken YT channels from the artist search. They can be skipped.
 | 
					                                // There may be broken YT channels from the artist search. They can be skipped.
 | 
				
			||||||
                                Ok(None)
 | 
					                                Ok(None)
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
                            MusicPageType::Track => unreachable!(),
 | 
					                            // Tracks were already handled above
 | 
				
			||||||
 | 
					                            MusicPageType::Track { .. } => unreachable!(),
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    None => Err("could not determine item type".to_owned()),
 | 
					                    None => Err("could not determine item type".to_owned()),
 | 
				
			||||||
| 
						 | 
					@ -655,7 +684,7 @@ impl MusicListMapper {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                match item.navigation_endpoint.music_page() {
 | 
					                match item.navigation_endpoint.music_page() {
 | 
				
			||||||
                    Some((page_type, id)) => match page_type {
 | 
					                    Some((page_type, id)) => match page_type {
 | 
				
			||||||
                        MusicPageType::Track => {
 | 
					                        MusicPageType::Track { is_video } => {
 | 
				
			||||||
                            let artists = map_artists(subtitle_p1).0;
 | 
					                            let artists = map_artists(subtitle_p1).0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                            self.items.push(MusicItem::Track(TrackItem {
 | 
					                            self.items.push(MusicItem::Track(TrackItem {
 | 
				
			||||||
| 
						 | 
					@ -669,7 +698,7 @@ impl MusicListMapper {
 | 
				
			||||||
                                view_count: subtitle_p2.and_then(|c| {
 | 
					                                view_count: subtitle_p2.and_then(|c| {
 | 
				
			||||||
                                    util::parse_large_numstr(c.first_str(), self.lang)
 | 
					                                    util::parse_large_numstr(c.first_str(), self.lang)
 | 
				
			||||||
                                }),
 | 
					                                }),
 | 
				
			||||||
                                is_video: true,
 | 
					                                is_video,
 | 
				
			||||||
                                track_nr: None,
 | 
					                                track_nr: None,
 | 
				
			||||||
                            }));
 | 
					                            }));
 | 
				
			||||||
                            Ok(Some(MusicEntityType::Track))
 | 
					                            Ok(Some(MusicEntityType::Track))
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -35,6 +35,8 @@ pub(crate) struct WatchEndpoint {
 | 
				
			||||||
    pub playlist_id: Option<String>,
 | 
					    pub playlist_id: Option<String>,
 | 
				
			||||||
    #[serde(default)]
 | 
					    #[serde(default)]
 | 
				
			||||||
    pub start_time_seconds: u32,
 | 
					    pub start_time_seconds: u32,
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
 | 
					    pub watch_endpoint_music_supported_configs: WatchEndpointConfigWrap,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Debug)]
 | 
					#[derive(Debug)]
 | 
				
			||||||
| 
						 | 
					@ -118,6 +120,30 @@ pub(crate) struct WebCommandMetadata {
 | 
				
			||||||
    pub web_page_type: PageType,
 | 
					    pub web_page_type: PageType,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Default, Debug, Deserialize)]
 | 
				
			||||||
 | 
					#[serde(rename_all = "camelCase")]
 | 
				
			||||||
 | 
					pub(crate) struct WatchEndpointConfigWrap {
 | 
				
			||||||
 | 
					    pub watch_endpoint_music_config: WatchEndpointConfig,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[serde_as]
 | 
				
			||||||
 | 
					#[derive(Default, Debug, Deserialize)]
 | 
				
			||||||
 | 
					#[serde(rename_all = "camelCase")]
 | 
				
			||||||
 | 
					pub(crate) struct WatchEndpointConfig {
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
 | 
					    #[serde_as(deserialize_as = "DefaultOnError")]
 | 
				
			||||||
 | 
					    pub music_video_type: MusicVideoType,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Default, Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
 | 
				
			||||||
 | 
					pub(crate) enum MusicVideoType {
 | 
				
			||||||
 | 
					    #[default]
 | 
				
			||||||
 | 
					    #[serde(rename = "MUSIC_VIDEO_TYPE_OMV")]
 | 
				
			||||||
 | 
					    Video,
 | 
				
			||||||
 | 
					    #[serde(rename = "MUSIC_VIDEO_TYPE_ATV")]
 | 
				
			||||||
 | 
					    Track,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
 | 
					#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
 | 
				
			||||||
pub(crate) enum PageType {
 | 
					pub(crate) enum PageType {
 | 
				
			||||||
    #[serde(
 | 
					    #[serde(
 | 
				
			||||||
| 
						 | 
					@ -152,7 +178,7 @@ pub(crate) enum MusicPageType {
 | 
				
			||||||
    Artist,
 | 
					    Artist,
 | 
				
			||||||
    Album,
 | 
					    Album,
 | 
				
			||||||
    Playlist,
 | 
					    Playlist,
 | 
				
			||||||
    Track,
 | 
					    Track { is_video: bool },
 | 
				
			||||||
    None,
 | 
					    None,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -189,7 +215,16 @@ impl NavigationEndpoint {
 | 
				
			||||||
                    // Genre radios (e.g. "pop radio") will be skipped
 | 
					                    // Genre radios (e.g. "pop radio") will be skipped
 | 
				
			||||||
                    (MusicPageType::None, watch.video_id)
 | 
					                    (MusicPageType::None, watch.video_id)
 | 
				
			||||||
                } else {
 | 
					                } else {
 | 
				
			||||||
                    (MusicPageType::Track, watch.video_id)
 | 
					                    (
 | 
				
			||||||
 | 
					                        MusicPageType::Track {
 | 
				
			||||||
 | 
					                            is_video: watch
 | 
				
			||||||
 | 
					                                .watch_endpoint_music_supported_configs
 | 
				
			||||||
 | 
					                                .watch_endpoint_music_config
 | 
				
			||||||
 | 
					                                .music_video_type
 | 
				
			||||||
 | 
					                                == MusicVideoType::Video,
 | 
				
			||||||
 | 
					                        },
 | 
				
			||||||
 | 
					                        watch.video_id,
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -56,7 +56,7 @@ MusicAlbum(
 | 
				
			||||||
        name: "25",
 | 
					        name: "25",
 | 
				
			||||||
      )),
 | 
					      )),
 | 
				
			||||||
      view_count: None,
 | 
					      view_count: None,
 | 
				
			||||||
      is_video: false,
 | 
					      is_video: true,
 | 
				
			||||||
      track_nr: Some(1),
 | 
					      track_nr: Some(1),
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
    TrackItem(
 | 
					    TrackItem(
 | 
				
			||||||
| 
						 | 
					@ -76,7 +76,7 @@ MusicAlbum(
 | 
				
			||||||
        name: "25",
 | 
					        name: "25",
 | 
				
			||||||
      )),
 | 
					      )),
 | 
				
			||||||
      view_count: None,
 | 
					      view_count: None,
 | 
				
			||||||
      is_video: false,
 | 
					      is_video: true,
 | 
				
			||||||
      track_nr: Some(2),
 | 
					      track_nr: Some(2),
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
    TrackItem(
 | 
					    TrackItem(
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -64,7 +64,7 @@ MusicAlbum(
 | 
				
			||||||
        name: "Der Himmel reißt auf",
 | 
					        name: "Der Himmel reißt auf",
 | 
				
			||||||
      )),
 | 
					      )),
 | 
				
			||||||
      view_count: None,
 | 
					      view_count: None,
 | 
				
			||||||
      is_video: false,
 | 
					      is_video: true,
 | 
				
			||||||
      track_nr: Some(1),
 | 
					      track_nr: Some(1),
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -51,7 +51,7 @@ MusicAlbum(
 | 
				
			||||||
        name: "<Queendom2> FINAL",
 | 
					        name: "<Queendom2> FINAL",
 | 
				
			||||||
      )),
 | 
					      )),
 | 
				
			||||||
      view_count: None,
 | 
					      view_count: None,
 | 
				
			||||||
      is_video: false,
 | 
					      is_video: true,
 | 
				
			||||||
      track_nr: Some(1),
 | 
					      track_nr: Some(1),
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
    TrackItem(
 | 
					    TrackItem(
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -122,7 +122,7 @@ mod tests {
 | 
				
			||||||
            text::TextComponent::Text { text: "🎧Listen and download aespa's debut single \"Black Mamba\": ".to_owned() },
 | 
					            text::TextComponent::Text { text: "🎧Listen and download aespa's debut single \"Black Mamba\": ".to_owned() },
 | 
				
			||||||
            text::TextComponent::Web { text: "https://smarturl.it/aespa_BlackMamba".to_owned(), url: "https://www.youtube.com/redirect?event=video_description&redir_token=QUFFLUhqbFY1QmpQamJPSms0Z1FnVTlQUS00ZFhBZnBJZ3xBQ3Jtc0tuRGJBanludGoyRnphb2dZWVd3cUNnS3dEd0FnNHFOZEY1NHBJaHFmLXpaWUJwX3ZucDZxVnpGeHNGX1FpMzFkZW9jQkI2Mi1wNGJ1UVFNN3h1MnN3R3JLMzdxU01nZ01POHBGcmxHU2puSUk1WHRzQQ&q=https%3A%2F%2Fsmarturl.it%2Faespa_BlackMamba&v=ZeerrnuLi5E".to_owned() },
 | 
					            text::TextComponent::Web { text: "https://smarturl.it/aespa_BlackMamba".to_owned(), url: "https://www.youtube.com/redirect?event=video_description&redir_token=QUFFLUhqbFY1QmpQamJPSms0Z1FnVTlQUS00ZFhBZnBJZ3xBQ3Jtc0tuRGJBanludGoyRnphb2dZWVd3cUNnS3dEd0FnNHFOZEY1NHBJaHFmLXpaWUJwX3ZucDZxVnpGeHNGX1FpMzFkZW9jQkI2Mi1wNGJ1UVFNN3h1MnN3R3JLMzdxU01nZ01POHBGcmxHU2puSUk1WHRzQQ&q=https%3A%2F%2Fsmarturl.it%2Faespa_BlackMamba&v=ZeerrnuLi5E".to_owned() },
 | 
				
			||||||
            text::TextComponent::Text { text: "\n🐍The Debut Stage ".to_owned() },
 | 
					            text::TextComponent::Text { text: "\n🐍The Debut Stage ".to_owned() },
 | 
				
			||||||
            text::TextComponent::Video { text: "https://youtu.be/Ky5RT5oGg0w".to_owned(), video_id: "Ky5RT5oGg0w".to_owned(), start_time: 0 },
 | 
					            text::TextComponent::Video { text: "https://youtu.be/Ky5RT5oGg0w".to_owned(), video_id: "Ky5RT5oGg0w".to_owned(), start_time: 0, is_video: true },
 | 
				
			||||||
            text::TextComponent::Text { text: "\n\n🎟️ aespa Showcase SYNK in LA! Tickets now on sale: ".to_owned() },
 | 
					            text::TextComponent::Text { text: "\n\n🎟️ aespa Showcase SYNK in LA! Tickets now on sale: ".to_owned() },
 | 
				
			||||||
            text::TextComponent::Web { text: "https://www.ticketmaster.com/event/0A...".to_owned(), url: "https://www.youtube.com/redirect?event=video_description&redir_token=QUFFLUhqbFpUMEZiaXJWWkszaVZXaEM0emxWU1JQV3NoQXxBQ3Jtc0tuU2g4VWNPNE5UY3hoSWYtamFzX0h4bUVQLVJiRy1ubDZrTnh3MUpGdDNSaUo0ZlMyT3lUM28ycUVBdHJLMndGcDhla3BkOFpxSVFfOS1QdVJPVHBUTEV1LXpOV0J2QXdhV05lV210cEJtZUJMeHdaTQ&q=https%3A%2F%2Fwww.ticketmaster.com%2Fevent%2F0A005CCD9E871F6E&v=ZeerrnuLi5E".to_owned() },
 | 
					            text::TextComponent::Web { text: "https://www.ticketmaster.com/event/0A...".to_owned(), url: "https://www.youtube.com/redirect?event=video_description&redir_token=QUFFLUhqbFpUMEZiaXJWWkszaVZXaEM0emxWU1JQV3NoQXxBQ3Jtc0tuU2g4VWNPNE5UY3hoSWYtamFzX0h4bUVQLVJiRy1ubDZrTnh3MUpGdDNSaUo0ZlMyT3lUM28ycUVBdHJLMndGcDhla3BkOFpxSVFfOS1QdVJPVHBUTEV1LXpOV0J2QXdhV05lV210cEJtZUJMeHdaTQ&q=https%3A%2F%2Fwww.ticketmaster.com%2Fevent%2F0A005CCD9E871F6E&v=ZeerrnuLi5E".to_owned() },
 | 
				
			||||||
            text::TextComponent::Text { text: "\n\nSubscribe to aespa Official YouTube Channel!\n".to_owned() },
 | 
					            text::TextComponent::Text { text: "\n\nSubscribe to aespa Official YouTube Channel!\n".to_owned() },
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,6 +19,7 @@ SAttributed {
 | 
				
			||||||
                text: "aespa 에스파 'Black ...",
 | 
					                text: "aespa 에스파 'Black ...",
 | 
				
			||||||
                video_id: "Ky5RT5oGg0w",
 | 
					                video_id: "Ky5RT5oGg0w",
 | 
				
			||||||
                start_time: 0,
 | 
					                start_time: 0,
 | 
				
			||||||
 | 
					                is_video: true,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            Text {
 | 
					            Text {
 | 
				
			||||||
                text: "\n\n🎟\u{fe0f} aespa Showcase SYNK in LA! Tickets now on sale: ",
 | 
					                text: "\n\n🎟\u{fe0f} aespa Showcase SYNK in LA! Tickets now on sale: ",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,7 +6,7 @@ use serde::{Deserialize, Deserializer};
 | 
				
			||||||
use serde_with::{serde_as, DeserializeAs};
 | 
					use serde_with::{serde_as, DeserializeAs};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    client::response::url_endpoint::{NavigationEndpoint, PageType},
 | 
					    client::response::url_endpoint::{MusicVideoType, NavigationEndpoint, PageType},
 | 
				
			||||||
    model::UrlTarget,
 | 
					    model::UrlTarget,
 | 
				
			||||||
    util,
 | 
					    util,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -94,6 +94,8 @@ pub(crate) enum TextComponent {
 | 
				
			||||||
        text: String,
 | 
					        text: String,
 | 
				
			||||||
        video_id: String,
 | 
					        video_id: String,
 | 
				
			||||||
        start_time: u32,
 | 
					        start_time: u32,
 | 
				
			||||||
 | 
					        /// True if the item is a video, false if it is a YTM track
 | 
				
			||||||
 | 
					        is_video: bool,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    Browse {
 | 
					    Browse {
 | 
				
			||||||
        text: String,
 | 
					        text: String,
 | 
				
			||||||
| 
						 | 
					@ -164,6 +166,11 @@ fn map_text_component(text: String, nav: NavigationEndpoint) -> TextComponent {
 | 
				
			||||||
            text,
 | 
					            text,
 | 
				
			||||||
            video_id: w.video_id,
 | 
					            video_id: w.video_id,
 | 
				
			||||||
            start_time: w.start_time_seconds,
 | 
					            start_time: w.start_time_seconds,
 | 
				
			||||||
 | 
					            is_video: w
 | 
				
			||||||
 | 
					                .watch_endpoint_music_supported_configs
 | 
				
			||||||
 | 
					                .watch_endpoint_music_config
 | 
				
			||||||
 | 
					                .music_video_type
 | 
				
			||||||
 | 
					                == MusicVideoType::Video,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        None => match nav.browse_endpoint {
 | 
					        None => match nav.browse_endpoint {
 | 
				
			||||||
            Some(b) => TextComponent::Browse {
 | 
					            Some(b) => TextComponent::Browse {
 | 
				
			||||||
| 
						 | 
					@ -365,6 +372,7 @@ impl From<TextComponent> for crate::model::richtext::TextComponent {
 | 
				
			||||||
                text,
 | 
					                text,
 | 
				
			||||||
                video_id,
 | 
					                video_id,
 | 
				
			||||||
                start_time,
 | 
					                start_time,
 | 
				
			||||||
 | 
					                ..
 | 
				
			||||||
            } => Self::YouTube {
 | 
					            } => Self::YouTube {
 | 
				
			||||||
                text,
 | 
					                text,
 | 
				
			||||||
                target: UrlTarget::Video {
 | 
					                target: UrlTarget::Video {
 | 
				
			||||||
| 
						 | 
					@ -581,6 +589,7 @@ mod tests {
 | 
				
			||||||
                text: "DEEP",
 | 
					                text: "DEEP",
 | 
				
			||||||
                video_id: "wZIoIgz5mbs",
 | 
					                video_id: "wZIoIgz5mbs",
 | 
				
			||||||
                start_time: 0,
 | 
					                start_time: 0,
 | 
				
			||||||
 | 
					                is_video: true,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        "###);
 | 
					        "###);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -39,7 +39,7 @@ MusicAlbum(
 | 
				
			||||||
      track_nr: Some(1),
 | 
					      track_nr: Some(1),
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
    TrackItem(
 | 
					    TrackItem(
 | 
				
			||||||
      id: "lhPOMUjV4rE",
 | 
					      id: "Jz-26iiDuYs",
 | 
				
			||||||
      title: "Waldbrand",
 | 
					      title: "Waldbrand",
 | 
				
			||||||
      duration: Some(208),
 | 
					      duration: Some(208),
 | 
				
			||||||
      cover: [],
 | 
					      cover: [],
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -23,7 +23,7 @@ MusicAlbum(
 | 
				
			||||||
  by_va: false,
 | 
					  by_va: false,
 | 
				
			||||||
  tracks: [
 | 
					  tracks: [
 | 
				
			||||||
    TrackItem(
 | 
					    TrackItem(
 | 
				
			||||||
      id: "XX0epju-YvY",
 | 
					      id: "VU6lEv0PKAo",
 | 
				
			||||||
      title: "Der Himmel reißt auf",
 | 
					      title: "Der Himmel reißt auf",
 | 
				
			||||||
      duration: Some(183),
 | 
					      duration: Some(183),
 | 
				
			||||||
      cover: [],
 | 
					      cover: [],
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,7 +14,7 @@ MusicAlbum(
 | 
				
			||||||
  by_va: true,
 | 
					  by_va: true,
 | 
				
			||||||
  tracks: [
 | 
					  tracks: [
 | 
				
			||||||
    TrackItem(
 | 
					    TrackItem(
 | 
				
			||||||
      id: "8IqLxg0GqXc",
 | 
					      id: "Tzai7JXo45w",
 | 
				
			||||||
      title: "Waka Boom (My Way) (feat. Lee Young Ji)",
 | 
					      title: "Waka Boom (My Way) (feat. Lee Young Ji)",
 | 
				
			||||||
      duration: Some(274),
 | 
					      duration: Some(274),
 | 
				
			||||||
      cover: [],
 | 
					      cover: [],
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1600,6 +1600,8 @@ async fn music_search_videos() {
 | 
				
			||||||
    assert_next(res.items, rp.query(), 15, 2).await;
 | 
					    assert_next(res.items, rp.query(), 15, 2).await;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					This podcast was removed from YouTube Music and I could not find another one
 | 
				
			||||||
#[tokio::test]
 | 
					#[tokio::test]
 | 
				
			||||||
async fn music_search_episode() {
 | 
					async fn music_search_episode() {
 | 
				
			||||||
    let rp = RustyPipe::builder().strict().build();
 | 
					    let rp = RustyPipe::builder().strict().build();
 | 
				
			||||||
| 
						 | 
					@ -1624,6 +1626,7 @@ async fn music_search_episode() {
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    assert!(!track.cover.is_empty(), "got no cover");
 | 
					    assert!(!track.cover.is_empty(), "got no cover");
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[rstest]
 | 
					#[rstest]
 | 
				
			||||||
#[case::single(
 | 
					#[case::single(
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue