Compare commits

...

3 commits

Author SHA1 Message Date
bd936a8c42 feat: music search suggestions 2022-11-25 10:33:49 +01:00
ef86181627 test: add music search radio test 2022-11-25 09:19:24 +01:00
fc8bce43fd fix: music item mapping, small refactor
fix: music item mapping, small refactor
2022-11-25 09:10:25 +01:00
18 changed files with 9193 additions and 209 deletions

View file

@ -23,7 +23,7 @@ inspired by [NewPipe](https://github.com/TeamNewPipe/NewPipeExtractor).
- [X] **Album**
- [X] **Artist**
- [X] **Search**
- [ ] **Search suggestions**
- [X] **Search suggestions**
- [X] **Radio**
- [X] **Track details** (lyrics, recommendations)
- [ ] **Moods**

View file

@ -48,6 +48,7 @@ pub async fn download_testfiles(project_root: &Path) {
music_search_artists(&testfiles).await;
music_search_playlists(&testfiles).await;
music_search_cont(&testfiles).await;
music_search_suggestion(&testfiles).await;
music_artist(&testfiles).await;
music_details(&testfiles).await;
music_lyrics(&testfiles).await;
@ -585,10 +586,14 @@ async fn music_album(testfiles: &Path) {
}
async fn music_search(testfiles: &Path) {
for (name, query) in [("default", "black mamba"), ("typo", "liblingsmensch")] {
for (name, query) in [
("default", "black mamba"),
("typo", "liblingsmensch"),
("radio", "pop radio"),
] {
let mut json_path = testfiles.to_path_buf();
json_path.push("music_search");
json_path.push(format!("{}.json", name));
json_path.push(format!("main_{}.json", name));
if json_path.exists() {
continue;
}
@ -684,6 +689,20 @@ async fn music_search_cont(testfiles: &Path) {
res.items.next(&rp.query()).await.unwrap().unwrap();
}
async fn music_search_suggestion(testfiles: &Path) {
for (name, query) in [("default", "t"), ("empty", "reujbhevmfndxnjrze")] {
let mut json_path = testfiles.to_path_buf();
json_path.push("music_search");
json_path.push(format!("suggestion_{}.json", name));
if json_path.exists() {
continue;
}
let rp = rp_testfile(&json_path);
rp.query().music_search_suggestion(query).await.unwrap();
}
}
async fn music_artist(testfiles: &Path) {
for (name, id) in [
("default", "UClmXPfaYhXOYsNn_QUyheWQ"),

View file

@ -24,6 +24,13 @@ struct QSearch<'a> {
params: Option<Params>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct QSearchSuggestion<'a> {
context: YTContext<'a>,
input: &'a str,
}
#[derive(Debug, Serialize)]
enum Params {
#[serde(rename = "EgWKAQIIAWoMEAMQBBAJEA4QChAF")]
@ -182,6 +189,23 @@ impl RustyPipeQuery {
)
.await
}
pub async fn music_search_suggestion(&self, query: &str) -> Result<Vec<String>, Error> {
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
let request_body = QSearchSuggestion {
context,
input: query,
};
self.execute_request::<response::MusicSearchSuggestion, _, _>(
ClientType::DesktopMusic,
"music_search_suggestion",
query,
"music/get_search_suggestions",
&request_body,
)
.await
}
}
impl MapResponse<MusicSearchResult> for response::MusicSearch {
@ -293,6 +317,41 @@ impl<T: FromYtItem> MapResponse<MusicSearchFiltered<T>> for response::MusicSearc
}
}
impl MapResponse<Vec<String>> for response::MusicSearchSuggestion {
fn map_response(
self,
_id: &str,
_lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
) -> Result<MapResult<Vec<String>>, ExtractionError> {
let items = self
.contents
.into_iter()
.next()
.map(|content| {
content
.search_suggestions_section_renderer
.contents
.into_iter()
.filter_map(|itm| {
match itm {
response::music_search::SearchSuggestionItem::SearchSuggestionRenderer {
suggestion,
} => Some(suggestion),
response::music_search::SearchSuggestionItem::None => None,
}
})
.collect::<Vec<_>>()
})
.unwrap_or_default();
Ok(MapResult {
c: items,
warnings: Vec::new(),
})
}
}
#[cfg(test)]
mod tests {
use std::{fs::File, io::BufReader, path::Path};
@ -312,8 +371,9 @@ mod tests {
#[rstest]
#[case::default("default")]
#[case::typo("typo")]
fn map_music_search(#[case] name: &str) {
let filename = format!("testfiles/music_search/{}.json", name);
#[case::radio("radio")]
fn map_music_search_main(#[case] name: &str) {
let filename = format!("testfiles/music_search/main_{}.json", name);
let json_path = Path::new(&filename);
let json_file = File::open(json_path).unwrap();
@ -328,7 +388,7 @@ mod tests {
map_res.warnings
);
insta::assert_ron_snapshot!(format!("map_music_search_{}", name), map_res.c);
insta::assert_ron_snapshot!(format!("map_music_search_main_{}", name), map_res.c);
}
#[rstest]
@ -416,4 +476,26 @@ mod tests {
insta::assert_ron_snapshot!(format!("map_music_search_playlists_{}", name), map_res.c);
}
#[rstest]
#[case::default("default")]
#[case::empty("empty")]
fn map_music_search_suggestion(#[case] name: &str) {
let filename = format!("testfiles/music_search/suggestion_{}.json", name);
let json_path = Path::new(&filename);
let json_file = File::open(json_path).unwrap();
let suggestion: response::MusicSearchSuggestion =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<Vec<String>> =
suggestion.map_response("", Language::En, None).unwrap();
assert!(
map_res.warnings.is_empty(),
"deserialization/mapping warnings: {:?}",
map_res.warnings
);
insta::assert_ron_snapshot!(format!("map_music_search_suggestion_{}", name), map_res.c);
}
}

View file

@ -21,6 +21,7 @@ pub(crate) use music_details::MusicRelated;
pub(crate) use music_item::MusicContinuation;
pub(crate) use music_playlist::MusicPlaylist;
pub(crate) use music_search::MusicSearch;
pub(crate) use music_search::MusicSearchSuggestion;
pub(crate) use player::Player;
pub(crate) use playlist::Playlist;
pub(crate) use playlist::PlaylistCont;

View file

@ -15,7 +15,7 @@ use crate::{
};
use super::{
url_endpoint::{BrowseEndpointWrap, NavigationEndpoint, PageType},
url_endpoint::{BrowseEndpointWrap, MusicPageType, NavigationEndpoint, PageType},
ContentsRenderer, MusicContinuationData, Thumbnails, ThumbnailsWrap,
};
@ -408,104 +408,6 @@ impl MusicListMapper {
let c2 = columns.next();
let c3 = columns.next();
match item.navigation_endpoint {
// Artist / Album / Playlist
Some(ne) => {
let mut subtitle_parts = c2
.ok_or_else(|| "could not get subtitle".to_owned())?
.renderer
.text
.split(util::DOT_SEPARATOR)
.into_iter();
let (page_type, id) = match ne.music_page() {
Some(music_page) => music_page,
None => {
// Ignore radio items
if subtitle_parts.len() == 1 {
return Ok(None);
}
return Err("invalid navigation endpoint".to_string());
}
};
let title =
title.ok_or_else(|| format!("track {}: could not get title", id))?;
let subtitle_p1 = subtitle_parts.next();
let subtitle_p2 = subtitle_parts.next();
let subtitle_p3 = subtitle_parts.next();
match page_type {
PageType::Artist => {
let subscriber_count = subtitle_p2.and_then(|p| {
util::parse_large_numstr(p.first_str(), self.lang)
});
self.items.push(MusicItem::Artist(ArtistItem {
id,
name: title,
avatar: item.thumbnail.into(),
subscriber_count,
}));
Ok(Some(MusicEntityType::Artist))
}
PageType::Album => {
let album_type = subtitle_p1
.map(|st| map_album_type(st.first_str(), self.lang))
.unwrap_or_default();
let (artists, by_va) = map_artists(subtitle_p2);
let year = subtitle_p3
.and_then(|st| util::parse_numeric(st.first_str()).ok());
self.items.push(MusicItem::Album(AlbumItem {
id,
name: title,
cover: item.thumbnail.into(),
artists,
album_type,
year,
by_va,
}));
Ok(Some(MusicEntityType::Album))
}
PageType::Playlist => {
// Part 1 may be the "Playlist" label
let (channel_p, tcount_p) = match subtitle_p3 {
Some(_) => (subtitle_p2, subtitle_p3),
None => (subtitle_p1, subtitle_p2),
};
let from_ytm = channel_p
.as_ref()
.map(|p| p.first_str() == util::YT_MUSIC_NAME)
.unwrap_or_default();
let channel = channel_p.and_then(|p| {
p.0.into_iter().find_map(|c| ChannelId::try_from(c).ok())
});
let track_count =
tcount_p.and_then(|p| util::parse_numeric(p.first_str()).ok());
self.items.push(MusicItem::Playlist(MusicPlaylistItem {
id,
name: title,
thumbnail: item.thumbnail.into(),
channel,
track_count,
from_ytm,
}));
Ok(Some(MusicEntityType::Playlist))
}
PageType::Channel => {
// There may be broken YT channels from the artist search. They can be skipped.
Ok(None)
}
}
}
// Track
None => {
let first_tn = item
.thumbnail
.music_thumbnail_renderer
@ -513,14 +415,23 @@ impl MusicListMapper {
.thumbnails
.first();
let id = item
.playlist_item_data
.map(|d| d.video_id)
let pt_id = item
.navigation_endpoint
.and_then(|ne| ne.music_page())
.or_else(|| {
first_tn.and_then(|tn| util::video_id_from_thumbnail_url(&tn.url))
item.playlist_item_data
.map(|d| (MusicPageType::Track, d.video_id))
})
.ok_or_else(|| "no video id".to_owned())?;
.or_else(|| {
first_tn.and_then(|tn| {
util::video_id_from_thumbnail_url(&tn.url)
.map(|id| (MusicPageType::Track, id))
})
});
match pt_id {
// Track
Some((MusicPageType::Track, id)) => {
let title =
title.ok_or_else(|| format!("track {}: could not get title", id))?;
@ -633,6 +544,92 @@ impl MusicListMapper {
}));
Ok(Some(MusicEntityType::Track))
}
// Artist / Album / Playlist
Some((page_type, id)) => {
let mut subtitle_parts = c2
.ok_or_else(|| "could not get subtitle".to_owned())?
.renderer
.text
.split(util::DOT_SEPARATOR)
.into_iter();
let title =
title.ok_or_else(|| format!("track {}: could not get title", id))?;
let subtitle_p1 = subtitle_parts.next();
let subtitle_p2 = subtitle_parts.next();
let subtitle_p3 = subtitle_parts.next();
match page_type {
MusicPageType::Artist => {
let subscriber_count = subtitle_p2.and_then(|p| {
util::parse_large_numstr(p.first_str(), self.lang)
});
self.items.push(MusicItem::Artist(ArtistItem {
id,
name: title,
avatar: item.thumbnail.into(),
subscriber_count,
}));
Ok(Some(MusicEntityType::Artist))
}
MusicPageType::Album => {
let album_type = subtitle_p1
.map(|st| map_album_type(st.first_str(), self.lang))
.unwrap_or_default();
let (artists, by_va) = map_artists(subtitle_p2);
let year = subtitle_p3
.and_then(|st| util::parse_numeric(st.first_str()).ok());
self.items.push(MusicItem::Album(AlbumItem {
id,
name: title,
cover: item.thumbnail.into(),
artists,
album_type,
year,
by_va,
}));
Ok(Some(MusicEntityType::Album))
}
MusicPageType::Playlist => {
// Part 1 may be the "Playlist" label
let (channel_p, tcount_p) = match subtitle_p3 {
Some(_) => (subtitle_p2, subtitle_p3),
None => (subtitle_p1, subtitle_p2),
};
let from_ytm = channel_p
.as_ref()
.map(|p| p.first_str() == util::YT_MUSIC_NAME)
.unwrap_or_default();
let channel = channel_p.and_then(|p| {
p.0.into_iter().find_map(|c| ChannelId::try_from(c).ok())
});
let track_count =
tcount_p.and_then(|p| util::parse_numeric(p.first_str()).ok());
self.items.push(MusicItem::Playlist(MusicPlaylistItem {
id,
name: title,
thumbnail: item.thumbnail.into(),
channel,
track_count,
from_ytm,
}));
Ok(Some(MusicEntityType::Playlist))
}
MusicPageType::None => {
// There may be broken YT channels from the artist search. They can be skipped.
Ok(None)
}
MusicPageType::Track => unreachable!(),
}
}
None => Err("could not determine item type".to_owned()),
}
}
// Tile
@ -642,44 +639,45 @@ impl MusicListMapper {
let subtitle_p2 = subtitle_parts.next();
let subtitle_p3 = subtitle_parts.next();
match item.navigation_endpoint.watch_endpoint {
// Music video
Some(wep) => {
match item.navigation_endpoint.music_page() {
Some((page_type, id)) => match page_type {
MusicPageType::Track => {
let artists = map_artists(subtitle_p1).0;
self.items.push(MusicItem::Track(TrackItem {
id: wep.video_id,
id,
title: item.title,
duration: None,
cover: item.thumbnail_renderer.into(),
artist_id: artists.first().and_then(|a| a.id.to_owned()),
artists,
album: None,
view_count: subtitle_p2
.and_then(|c| util::parse_large_numstr(c.first_str(), self.lang)),
view_count: subtitle_p2.and_then(|c| {
util::parse_large_numstr(c.first_str(), self.lang)
}),
is_video: true,
track_nr: None,
}));
Ok(Some(MusicEntityType::Track))
}
// Artist / Album / Playlist
None => {
let (page_type, id) = item
.navigation_endpoint
.music_page()
.ok_or_else(|| "could not get navigation endpoint".to_owned())?;
MusicPageType::Artist => {
let subscriber_count = subtitle_p1
.and_then(|p| util::parse_large_numstr(p.first_str(), self.lang));
match page_type {
PageType::Album => {
self.items.push(MusicItem::Artist(ArtistItem {
id,
name: item.title,
avatar: item.thumbnail_renderer.into(),
subscriber_count,
}));
Ok(Some(MusicEntityType::Artist))
}
MusicPageType::Album => {
let mut year = None;
let mut album_type = AlbumType::Single;
let (artists, by_va) = match (
subtitle_p1,
subtitle_p2,
&self.artists,
self.artist_page,
) {
let (artists, by_va) =
match (subtitle_p1, subtitle_p2, &self.artists, self.artist_page) {
// "2022" (Artist singles)
(Some(year_txt), None, Some(artists), true) => {
year = util::parse_numeric(year_txt.first_str()).ok();
@ -717,7 +715,7 @@ impl MusicListMapper {
}));
Ok(Some(MusicEntityType::Album))
}
PageType::Playlist => {
MusicPageType::Playlist => {
let from_ytm = subtitle_p2
.as_ref()
.map(|p| p.first_str() == util::YT_MUSIC_NAME)
@ -725,8 +723,8 @@ impl MusicListMapper {
let channel = subtitle_p2.and_then(|p| {
p.0.into_iter().find_map(|c| ChannelId::try_from(c).ok())
});
let track_count = subtitle_p3
.and_then(|p| util::parse_numeric(p.first_str()).ok());
let track_count =
subtitle_p3.and_then(|p| util::parse_numeric(p.first_str()).ok());
self.items.push(MusicItem::Playlist(MusicPlaylistItem {
id,
@ -738,24 +736,9 @@ impl MusicListMapper {
}));
Ok(Some(MusicEntityType::Playlist))
}
PageType::Artist => {
let subscriber_count = subtitle_p1.and_then(|p| {
util::parse_large_numstr(p.first_str(), self.lang)
});
self.items.push(MusicItem::Artist(ArtistItem {
id,
name: item.title,
avatar: item.thumbnail_renderer.into(),
subscriber_count,
}));
Ok(Some(MusicEntityType::Artist))
}
PageType::Channel => {
Err(format!("channel items unsupported. id: {}", id))
}
}
}
MusicPageType::None => Ok(None),
},
None => Err("could not determine item type".to_owned()),
}
}
}

View file

@ -12,6 +12,16 @@ pub(crate) struct MusicSearch {
pub contents: Contents,
}
/// Response model for YouTube Music suggestion
#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct MusicSearchSuggestion {
#[serde(default)]
#[serde_as(as = "VecSkipError<_>")]
pub contents: Vec<SearchSuggestionsSection>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct Contents {
@ -45,3 +55,21 @@ pub(crate) struct ShowingResultsForRenderer {
#[serde_as(as = "Text")]
pub corrected_query: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct SearchSuggestionsSection {
pub search_suggestions_section_renderer: ContentsRenderer<SearchSuggestionItem>,
}
#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) enum SearchSuggestionItem {
SearchSuggestionRenderer {
#[serde_as(as = "Text")]
suggestion: String,
},
#[serde(other, deserialize_with = "deserialize_ignore_any")]
None,
}

View file

@ -32,6 +32,7 @@ pub(crate) struct NavigationEndpoint {
#[serde(rename_all = "camelCase")]
pub(crate) struct WatchEndpoint {
pub video_id: String,
pub playlist_id: Option<String>,
#[serde(default)]
pub start_time_seconds: u32,
}
@ -146,17 +147,51 @@ impl PageType {
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub(crate) enum MusicPageType {
Artist,
Album,
Playlist,
Track,
None,
}
impl From<PageType> for MusicPageType {
fn from(t: PageType) -> Self {
match t {
PageType::Artist => MusicPageType::Artist,
PageType::Album => MusicPageType::Album,
PageType::Playlist => MusicPageType::Playlist,
PageType::Channel => MusicPageType::None,
}
}
}
impl NavigationEndpoint {
pub(crate) fn music_page(self) -> Option<(PageType, String)> {
pub(crate) fn music_page(self) -> Option<(MusicPageType, String)> {
match self.browse_endpoint {
Some(browse) => match browse.browse_endpoint_context_supported_configs {
Some(config) => Some((
config.browse_endpoint_context_music_config.page_type,
config.browse_endpoint_context_music_config.page_type.into(),
browse.browse_id,
)),
None => None,
},
None => None,
}
.or_else(|| {
self.watch_endpoint.map(|watch| {
if watch
.playlist_id
.map(|plid| plid.starts_with("RDQM"))
.unwrap_or_default()
{
// Genre radios (e.g. "pop radio") will be skipped
(MusicPageType::None, watch.video_id)
} else {
(MusicPageType::Track, watch.video_id)
}
})
})
}
}

View file

@ -0,0 +1,497 @@
---
source: src/client/music_search.rs
expression: map_res.c
---
MusicSearchResult(
tracks: [
TrackItem(
id: "ITdJEc_81h4",
title: "Pop (Radio Version)",
duration: Some(176),
cover: [
Thumbnail(
url: "https://lh3.googleusercontent.com/gwNUEmnlDQimTO_eMcH4Dv-74PK_mcc00xyIk-3tzbW98KkRoD5ZGMhJHBNkZV-ExnTWfa-_ruQbcuM=w60-h60-s-l90-rj",
width: 60,
height: 60,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/gwNUEmnlDQimTO_eMcH4Dv-74PK_mcc00xyIk-3tzbW98KkRoD5ZGMhJHBNkZV-ExnTWfa-_ruQbcuM=w120-h120-s-l90-rj",
width: 120,
height: 120,
),
],
artists: [
ArtistId(
id: Some("UCm-wsxhI_OOhg4O1TwDJ98A"),
name: "*NSYNC",
),
],
artist_id: Some("UCm-wsxhI_OOhg4O1TwDJ98A"),
album: Some(AlbumId(
id: "MPREb_k2jVJAzQhba",
name: "Greatest Hits (Deluxe)",
)),
view_count: None,
is_video: false,
track_nr: None,
),
TrackItem(
id: "VHLPvrlclmQ",
title: "Pop im Radio",
duration: Some(224),
cover: [
Thumbnail(
url: "https://lh3.googleusercontent.com/OD-BVa7OhsLAQ-bV01DBNiBdzVecGxQ_kBvO7bsVJa6HBg9bVWdF7Izkmgs0E86RLRbKjYxVZTc4__o=w60-h60-l90-rj",
width: 60,
height: 60,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/OD-BVa7OhsLAQ-bV01DBNiBdzVecGxQ_kBvO7bsVJa6HBg9bVWdF7Izkmgs0E86RLRbKjYxVZTc4__o=w120-h120-l90-rj",
width: 120,
height: 120,
),
],
artists: [
ArtistId(
id: Some("UCTMksUr7ijdCL7U5wqjKcdA"),
name: "Michy Reincke",
),
],
artist_id: Some("UCTMksUr7ijdCL7U5wqjKcdA"),
album: Some(AlbumId(
id: "MPREb_2AXSzG1uDh5",
name: "Das böse Glück (Bonus Edition)",
)),
view_count: None,
is_video: false,
track_nr: None,
),
TrackItem(
id: "R9TPed_ohKM",
title: "POP!",
duration: Some(169),
cover: [
Thumbnail(
url: "https://lh3.googleusercontent.com/Xdwe3OXj4qkOv5P_FCNWqSf3cx1VnAfAtB6dD8g1v04ReAcxHm6KAtA08CzPSnbKph-9DwrIMGcRtwFx=w60-h60-l90-rj",
width: 60,
height: 60,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/Xdwe3OXj4qkOv5P_FCNWqSf3cx1VnAfAtB6dD8g1v04ReAcxHm6KAtA08CzPSnbKph-9DwrIMGcRtwFx=w120-h120-l90-rj",
width: 120,
height: 120,
),
],
artists: [
ArtistId(
id: Some("UCBznuF9zIIbRS9Y1Yu4yOhg"),
name: "NAYEON",
),
],
artist_id: Some("UCBznuF9zIIbRS9Y1Yu4yOhg"),
album: Some(AlbumId(
id: "MPREb_pBK5MaK36C5",
name: "IM NAYEON",
)),
view_count: None,
is_video: false,
track_nr: None,
),
TrackItem(
id: "Ej1nxBxFSKc",
title: "Non-Stop-Pop FM (Hosted by Cara Delevingne) [Grand Theft Auto V] | Pop, R&B, Dance-pop Music Mix",
duration: Some(8752),
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/Ej1nxBxFSKc/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3nyY9c1BovrvjSAa2rjVSmcTZZcNA",
width: 400,
height: 225,
),
],
artists: [
ArtistId(
id: Some("UC6ZVjGqRf7elKAcYTXCaIsw"),
name: "Listen To This",
),
],
artist_id: Some("UC6ZVjGqRf7elKAcYTXCaIsw"),
album: None,
view_count: Some(2400000),
is_video: true,
track_nr: None,
),
TrackItem(
id: "26OrUhkRa3c",
title: "Top Hits 2020 Video Mix (CLEAN) | Hip Hop 2020 - (POP HITS 2020, TOP 40 HITS, BEST POP HITS,TOP 40)",
duration: Some(10012),
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/26OrUhkRa3c/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3mtPP2bCRAx6JAsJ3NGPbFrs06n6w",
width: 400,
height: 225,
),
],
artists: [
ArtistId(
id: Some("UCM7nREGFBumYELglbtUL8FA"),
name: "Top Music",
),
],
artist_id: Some("UCM7nREGFBumYELglbtUL8FA"),
album: None,
view_count: Some(2100000),
is_video: true,
track_nr: None,
),
TrackItem(
id: "Idk-oFqn3kM",
title: "THE BEST CHARTS POP HITS 2021 I THE BEST MUSIC RADIO CHARTS I",
duration: Some(8795),
cover: [
Thumbnail(
url: "https://i.ytimg.com/vi/Idk-oFqn3kM/hqdefault.jpg?sqp=-oaymwEWCMACELQBIAQqCghQEJADGFogjgJIWg&rs=AMzJL3lm-ElqoCByIARJE5_7xs7jLv9AHA",
width: 320,
height: 180,
),
],
artists: [
ArtistId(
id: Some("UCOVD3PtbJGiAcp-c6opijoQ"),
name: "SCHLAGER AKTUELL",
),
],
artist_id: Some("UCOVD3PtbJGiAcp-c6opijoQ"),
album: None,
view_count: Some(67000),
is_video: true,
track_nr: None,
),
],
albums: [
AlbumItem(
id: "MPREb_CYbQPbuAWrt",
name: "Pop Radio",
cover: [
Thumbnail(
url: "https://lh3.googleusercontent.com/Lh8ESFXrI084BAHjCQTPTAwtMRxDiU3NqfCDNT0IHrG6s8eqPHzPbY5O5SumZaxwjq2g4EEtPIak47Sm=w60-h60-l90-rj",
width: 60,
height: 60,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/Lh8ESFXrI084BAHjCQTPTAwtMRxDiU3NqfCDNT0IHrG6s8eqPHzPbY5O5SumZaxwjq2g4EEtPIak47Sm=w120-h120-l90-rj",
width: 120,
height: 120,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/Lh8ESFXrI084BAHjCQTPTAwtMRxDiU3NqfCDNT0IHrG6s8eqPHzPbY5O5SumZaxwjq2g4EEtPIak47Sm=w226-h226-l90-rj",
width: 226,
height: 226,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/Lh8ESFXrI084BAHjCQTPTAwtMRxDiU3NqfCDNT0IHrG6s8eqPHzPbY5O5SumZaxwjq2g4EEtPIak47Sm=w544-h544-l90-rj",
width: 544,
height: 544,
),
],
artists: [],
album_type: Album,
year: Some(2016),
by_va: true,
),
AlbumItem(
id: "MPREb_Cmf1lWfv0dV",
name: "Pop Radio",
cover: [
Thumbnail(
url: "https://lh3.googleusercontent.com/f0kOUZ3WurOC8qwxQ0JHtWhrmOGzwANS0x23Yw7iK9OdIvct4kMjJwNHla99_AI96-JBHwcq4Afs6rI=w60-h60-l90-rj",
width: 60,
height: 60,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/f0kOUZ3WurOC8qwxQ0JHtWhrmOGzwANS0x23Yw7iK9OdIvct4kMjJwNHla99_AI96-JBHwcq4Afs6rI=w120-h120-l90-rj",
width: 120,
height: 120,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/f0kOUZ3WurOC8qwxQ0JHtWhrmOGzwANS0x23Yw7iK9OdIvct4kMjJwNHla99_AI96-JBHwcq4Afs6rI=w226-h226-l90-rj",
width: 226,
height: 226,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/f0kOUZ3WurOC8qwxQ0JHtWhrmOGzwANS0x23Yw7iK9OdIvct4kMjJwNHla99_AI96-JBHwcq4Afs6rI=w544-h544-l90-rj",
width: 544,
height: 544,
),
],
artists: [],
album_type: Album,
year: Some(2022),
by_va: true,
),
AlbumItem(
id: "MPREb_Ic1ZUsaeuRv",
name: "Pop Radio",
cover: [
Thumbnail(
url: "https://lh3.googleusercontent.com/AFWgHYm5Q7LdNo83TGXQWVApLntgB76Z8Vdf5wBMCxVhzwzcInS0uo2S9E_c6d9brP9MXjkAZW0X4EQ=w60-h60-l90-rj",
width: 60,
height: 60,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/AFWgHYm5Q7LdNo83TGXQWVApLntgB76Z8Vdf5wBMCxVhzwzcInS0uo2S9E_c6d9brP9MXjkAZW0X4EQ=w120-h120-l90-rj",
width: 120,
height: 120,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/AFWgHYm5Q7LdNo83TGXQWVApLntgB76Z8Vdf5wBMCxVhzwzcInS0uo2S9E_c6d9brP9MXjkAZW0X4EQ=w226-h226-l90-rj",
width: 226,
height: 226,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/AFWgHYm5Q7LdNo83TGXQWVApLntgB76Z8Vdf5wBMCxVhzwzcInS0uo2S9E_c6d9brP9MXjkAZW0X4EQ=w544-h544-l90-rj",
width: 544,
height: 544,
),
],
artists: [
ArtistId(
id: Some("UCG7LUZBrK6GcfTwowTeTiOQ"),
name: "Strange Radio",
),
],
album_type: Album,
year: Some(2002),
by_va: false,
),
],
artists: [
ArtistItem(
id: "UCSZJrhZ2_ILCpyk3Z3AZVTA",
name: "Icona Pop",
avatar: [
Thumbnail(
url: "https://lh3.googleusercontent.com/sw3ilLK-equKu_EtKG1ehnbNqbmo55ZqS_LjOlu4SuYRQrGyWoxIMF9OSw4ORpVtgYlKoeJGD4thG7k=w60-h60-p-l90-rj",
width: 60,
height: 60,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/sw3ilLK-equKu_EtKG1ehnbNqbmo55ZqS_LjOlu4SuYRQrGyWoxIMF9OSw4ORpVtgYlKoeJGD4thG7k=w120-h120-p-l90-rj",
width: 120,
height: 120,
),
],
subscriber_count: Some(713000),
),
ArtistItem(
id: "UCOk0CLydqB-B0UH7UaZrVqw",
name: "Bacilos",
avatar: [
Thumbnail(
url: "https://lh3.googleusercontent.com/HtQH-8U0IvzGUjkEzOZjpLEBdqcEBaSRAmpneHhtXbiZHL1rJsoq8iJFwcCSMY7PlM-UuzVGDkoJn6k=w60-h60-l90-rj",
width: 60,
height: 60,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/HtQH-8U0IvzGUjkEzOZjpLEBdqcEBaSRAmpneHhtXbiZHL1rJsoq8iJFwcCSMY7PlM-UuzVGDkoJn6k=w120-h120-l90-rj",
width: 120,
height: 120,
),
],
subscriber_count: Some(339000),
),
ArtistItem(
id: "UC-Unifbw_ADqgIeMq4AdvvA",
name: "Death Pop Radio",
avatar: [
Thumbnail(
url: "https://lh3.googleusercontent.com/P8DRNrrBP_x4GmPXXKJkWKroLuMCpGW4DJTgxFPYFI-MlLk3pI6xOYMpMnzyb49md-8VVn9L3RHNTfMq=w60-h60-l90-rj",
width: 60,
height: 60,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/P8DRNrrBP_x4GmPXXKJkWKroLuMCpGW4DJTgxFPYFI-MlLk3pI6xOYMpMnzyb49md-8VVn9L3RHNTfMq=w120-h120-l90-rj",
width: 120,
height: 120,
),
],
subscriber_count: Some(11),
),
],
playlists: [
MusicPlaylistItem(
id: "RDCLAK5uy_l8kJfTElp2zFMop7IboOXetbbKU3a9VeQ",
name: "REST Turkish Rap",
thumbnail: [
Thumbnail(
url: "https://lh3.googleusercontent.com/xhb5UI1wFvzj6g6llLK2GBBxL1M_ozXRIMXDOIcMyeVY3yG58_qnqRbfbpqlI-C2wUaGU9_re5yC7Tzg=w60-h60-l90-rj",
width: 60,
height: 60,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/xhb5UI1wFvzj6g6llLK2GBBxL1M_ozXRIMXDOIcMyeVY3yG58_qnqRbfbpqlI-C2wUaGU9_re5yC7Tzg=w120-h120-l90-rj",
width: 120,
height: 120,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/xhb5UI1wFvzj6g6llLK2GBBxL1M_ozXRIMXDOIcMyeVY3yG58_qnqRbfbpqlI-C2wUaGU9_re5yC7Tzg=w226-h226-l90-rj",
width: 226,
height: 226,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/xhb5UI1wFvzj6g6llLK2GBBxL1M_ozXRIMXDOIcMyeVY3yG58_qnqRbfbpqlI-C2wUaGU9_re5yC7Tzg=w544-h544-l90-rj",
width: 544,
height: 544,
),
],
channel: None,
track_count: Some(50),
from_ytm: true,
),
MusicPlaylistItem(
id: "RDCLAK5uy_kLB769E3eFSzgy4fbpu6-1YPLh90b0JAY",
name: "Pop Hotlist",
thumbnail: [
Thumbnail(
url: "https://lh3.googleusercontent.com/YlPXlLEWrIQjBJ37sKN96YLw8x5nDpPgqGWaUmOft0S0C0arw-MJr3cKvKzWGjLAtDxCTIA_Uobx9sA=w60-h60-l90-rj",
width: 60,
height: 60,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/YlPXlLEWrIQjBJ37sKN96YLw8x5nDpPgqGWaUmOft0S0C0arw-MJr3cKvKzWGjLAtDxCTIA_Uobx9sA=w120-h120-l90-rj",
width: 120,
height: 120,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/YlPXlLEWrIQjBJ37sKN96YLw8x5nDpPgqGWaUmOft0S0C0arw-MJr3cKvKzWGjLAtDxCTIA_Uobx9sA=w226-h226-l90-rj",
width: 226,
height: 226,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/YlPXlLEWrIQjBJ37sKN96YLw8x5nDpPgqGWaUmOft0S0C0arw-MJr3cKvKzWGjLAtDxCTIA_Uobx9sA=w544-h544-l90-rj",
width: 544,
height: 544,
),
],
channel: None,
track_count: Some(54),
from_ytm: true,
),
MusicPlaylistItem(
id: "RDCLAK5uy_mCvOm3kQy1RTBwDOGYkNhtHwMO89ffquk",
name: "Crème French Pop",
thumbnail: [
Thumbnail(
url: "https://lh3.googleusercontent.com/Tnkqaz7qIHSzvdyK2UqNQZCcV9fCKfc98a4FoN0iD1cPMn6j_8apdd0ukTdbe2Dlu11EnV1QuYRuGgE=w60-h60-l90-rj",
width: 60,
height: 60,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/Tnkqaz7qIHSzvdyK2UqNQZCcV9fCKfc98a4FoN0iD1cPMn6j_8apdd0ukTdbe2Dlu11EnV1QuYRuGgE=w120-h120-l90-rj",
width: 120,
height: 120,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/Tnkqaz7qIHSzvdyK2UqNQZCcV9fCKfc98a4FoN0iD1cPMn6j_8apdd0ukTdbe2Dlu11EnV1QuYRuGgE=w226-h226-l90-rj",
width: 226,
height: 226,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/Tnkqaz7qIHSzvdyK2UqNQZCcV9fCKfc98a4FoN0iD1cPMn6j_8apdd0ukTdbe2Dlu11EnV1QuYRuGgE=w544-h544-l90-rj",
width: 544,
height: 544,
),
],
channel: None,
track_count: Some(50),
from_ytm: true,
),
MusicPlaylistItem(
id: "PL47aILYuQXEKiHdqMfNCHat1Gck3XQrrK",
name: "Today\'s Pop Hits Playlist 2022 ♫ Best Radio Hits 2022",
thumbnail: [
Thumbnail(
url: "https://yt3.googleusercontent.com/l9yiCtp9NGGXn397Jybr_7_4I8TvjKpp9XG54Tv8ZfwkimDWvCfSJXNTf-x9XlgSzsOxdh0doJw=s192",
width: 192,
height: 192,
),
Thumbnail(
url: "https://yt3.googleusercontent.com/l9yiCtp9NGGXn397Jybr_7_4I8TvjKpp9XG54Tv8ZfwkimDWvCfSJXNTf-x9XlgSzsOxdh0doJw=s576",
width: 576,
height: 576,
),
Thumbnail(
url: "https://yt3.googleusercontent.com/l9yiCtp9NGGXn397Jybr_7_4I8TvjKpp9XG54Tv8ZfwkimDWvCfSJXNTf-x9XlgSzsOxdh0doJw=s1200",
width: 1200,
height: 1200,
),
],
channel: Some(ChannelId(
id: "UCEYgc2eKzQXQ9OGCuT4JVPQ",
name: "Redlist - International Playlists",
)),
track_count: Some(100),
from_ytm: false,
),
MusicPlaylistItem(
id: "PL5ITQ2Yq_HLpidRR3wAio-YRBnG7-FeLd",
name: "Radio Swiss Pop",
thumbnail: [
Thumbnail(
url: "https://i.ytimg.com/vi/MBH-QbN5BcQ/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3nIIqml8U-wrYWs5ZG8jbBPxOahQA",
width: 400,
height: 225,
),
Thumbnail(
url: "https://i.ytimg.com/vi/MBH-QbN5BcQ/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3ksKy8FmdwpeWF52rPYk16zPYN4pg",
width: 800,
height: 450,
),
Thumbnail(
url: "https://i.ytimg.com/vi/MBH-QbN5BcQ/hq720.jpg?sqp=-oaymwEXCNUGEOADIAQqCwjVARCqCBh4INgESFo&rs=AMzJL3lfMhiN7qWWYCkvyhloPNJsjK6p5g",
width: 853,
height: 480,
),
],
channel: Some(ChannelId(
id: "UCjD0UddJFWadpaTrBJPOVGw",
name: "Raphaël Weissreiner",
)),
track_count: Some(176),
from_ytm: false,
),
MusicPlaylistItem(
id: "PLX6L4t7t6ZanfCJ1wBxRdGZ_mk9ygmKqo",
name: "Deutsch Pop Hits NEU 2022",
thumbnail: [
Thumbnail(
url: "https://yt3.ggpht.com/AhXFgxhzyIQumeUtEasnjczPfMXZLWu5gBNlWW_z-Evb0sbcJLPHTMuKzy0cbsBHqDhDSNA7Lg=s192",
width: 192,
height: 192,
),
Thumbnail(
url: "https://yt3.ggpht.com/AhXFgxhzyIQumeUtEasnjczPfMXZLWu5gBNlWW_z-Evb0sbcJLPHTMuKzy0cbsBHqDhDSNA7Lg=s576",
width: 576,
height: 576,
),
Thumbnail(
url: "https://yt3.ggpht.com/AhXFgxhzyIQumeUtEasnjczPfMXZLWu5gBNlWW_z-Evb0sbcJLPHTMuKzy0cbsBHqDhDSNA7Lg=s1200",
width: 1200,
height: 1200,
),
],
channel: Some(ChannelId(
id: "UCesP91XKnuZVd6OJN06hokg",
name: "Startup Records",
)),
track_count: Some(171),
from_ytm: false,
),
],
corrected_query: None,
order: [
Track,
Album,
Playlist,
Artist,
],
)

View file

@ -0,0 +1,13 @@
---
source: src/client/music_search.rs
expression: map_res.c
---
[
"taylor swift",
"tkkg",
"techno",
"t low",
"the weeknd",
"tiktok songs",
"toten hosen",
]

View file

@ -0,0 +1,5 @@
---
source: src/client/music_search.rs
expression: map_res.c
---
[]

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,242 @@
{
"contents": [
{
"searchSuggestionsSectionRenderer": {
"contents": [
{
"searchSuggestionRenderer": {
"icon": {
"iconType": "SEARCH"
},
"navigationEndpoint": {
"clickTrackingParams": "CAcQpWEYASITCKbj7Pb3yPsCFcfUEQgdRe0MBA==",
"searchEndpoint": {
"query": "taylor swift"
}
},
"suggestion": {
"runs": [
{
"bold": true,
"text": "t"
},
{
"text": "aylor swift"
}
]
},
"trackingParams": "CAcQpWEYASITCKbj7Pb3yPsCFcfUEQgdRe0MBA=="
}
},
{
"searchSuggestionRenderer": {
"icon": {
"iconType": "SEARCH"
},
"navigationEndpoint": {
"clickTrackingParams": "CAYQpWEYAiITCKbj7Pb3yPsCFcfUEQgdRe0MBA==",
"searchEndpoint": {
"query": "tkkg"
}
},
"suggestion": {
"runs": [
{
"bold": true,
"text": "t"
},
{
"text": "kkg"
}
]
},
"trackingParams": "CAYQpWEYAiITCKbj7Pb3yPsCFcfUEQgdRe0MBA=="
}
},
{
"searchSuggestionRenderer": {
"icon": {
"iconType": "SEARCH"
},
"navigationEndpoint": {
"clickTrackingParams": "CAUQpWEYAyITCKbj7Pb3yPsCFcfUEQgdRe0MBA==",
"searchEndpoint": {
"query": "techno"
}
},
"suggestion": {
"runs": [
{
"bold": true,
"text": "t"
},
{
"text": "echno"
}
]
},
"trackingParams": "CAUQpWEYAyITCKbj7Pb3yPsCFcfUEQgdRe0MBA=="
}
},
{
"searchSuggestionRenderer": {
"icon": {
"iconType": "SEARCH"
},
"navigationEndpoint": {
"clickTrackingParams": "CAQQpWEYBCITCKbj7Pb3yPsCFcfUEQgdRe0MBA==",
"searchEndpoint": {
"query": "t low"
}
},
"suggestion": {
"runs": [
{
"bold": true,
"text": "t"
},
{
"text": " low"
}
]
},
"trackingParams": "CAQQpWEYBCITCKbj7Pb3yPsCFcfUEQgdRe0MBA=="
}
},
{
"searchSuggestionRenderer": {
"icon": {
"iconType": "SEARCH"
},
"navigationEndpoint": {
"clickTrackingParams": "CAMQpWEYBSITCKbj7Pb3yPsCFcfUEQgdRe0MBA==",
"searchEndpoint": {
"query": "the weeknd"
}
},
"suggestion": {
"runs": [
{
"bold": true,
"text": "t"
},
{
"text": "he weeknd"
}
]
},
"trackingParams": "CAMQpWEYBSITCKbj7Pb3yPsCFcfUEQgdRe0MBA=="
}
},
{
"searchSuggestionRenderer": {
"icon": {
"iconType": "SEARCH"
},
"navigationEndpoint": {
"clickTrackingParams": "CAIQpWEYBiITCKbj7Pb3yPsCFcfUEQgdRe0MBA==",
"searchEndpoint": {
"query": "tiktok songs"
}
},
"suggestion": {
"runs": [
{
"bold": true,
"text": "t"
},
{
"text": "iktok songs"
}
]
},
"trackingParams": "CAIQpWEYBiITCKbj7Pb3yPsCFcfUEQgdRe0MBA=="
}
},
{
"searchSuggestionRenderer": {
"icon": {
"iconType": "SEARCH"
},
"navigationEndpoint": {
"clickTrackingParams": "CAEQpWEYByITCKbj7Pb3yPsCFcfUEQgdRe0MBA==",
"searchEndpoint": {
"query": "toten hosen"
}
},
"suggestion": {
"runs": [
{
"bold": true,
"text": "t"
},
{
"text": "oten hosen"
}
]
},
"trackingParams": "CAEQpWEYByITCKbj7Pb3yPsCFcfUEQgdRe0MBA=="
}
}
]
}
}
],
"responseContext": {
"serviceTrackingParams": [
{
"params": [
{
"key": "c",
"value": "WEB_REMIX"
},
{
"key": "cver",
"value": "1.20221121.01.00"
},
{
"key": "yt_li",
"value": "0"
},
{
"key": "GetMusicSearchSuggestions_rid",
"value": "0xe343e5421f9bc4f5"
}
],
"service": "CSI"
},
{
"params": [
{
"key": "logged_in",
"value": "0"
},
{
"key": "e",
"value": "1714247,9407157,23804281,23882685,23885487,23918597,23934970,23946420,23966208,23983296,23998056,24001373,24002022,24002025,24004644,24007246,24034168,24036948,24077241,24080738,24108448,24120820,24135310,24140247,24161116,24162920,24164186,24169501,24181174,24187043,24187377,24191629,24197450,24199724,24200839,24211178,24217535,24219713,24237297,24241378,24255165,24255543,24255545,24257695,24262346,24263796,24265426,24267564,24268142,24271464,24279196,24282724,24287327,24287604,24288043,24288442,24290971,24292955,24293803,24296354,24298084,24299747,24299875,24390374,24390675,24391018,24391541,24391579,24392268,24392403,24392452,24398048,24401557,24402891,24405024,24406605,24407200,24407452,24407665,24410273,24410853,24412682,24412897,24413820,24414162,24414917,24415579,24415866,24416290,24419371,24420756,24421162,24421894,24422904,24424806,24590921,39322504,39322574"
}
],
"service": "GFEEDBACK"
},
{
"params": [
{
"key": "client.version",
"value": "1.20000101"
},
{
"key": "client.name",
"value": "WEB_REMIX"
},
{
"key": "client.fexp",
"value": "24257695,24288043,24415579,23882685,24412682,39322574,24287604,24410273,23885487,24217535,24392452,1714247,9407157,24390675,24299747,24401557,39322504,24410853,24191629,24108448,24279196,24255545,24292955,24001373,24161116,24290971,24413820,23934970,24007246,24187377,23998056,24391018,24415866,24293803,24262346,24282724,24398048,24407452,24187043,24407200,24419371,24199724,24421162,24211178,24287327,24265426,24392268,24164186,24241378,24391541,24422904,24255165,24402891,24140247,24416290,23983296,24002025,24414917,24414162,24391579,24237297,24406605,24424806,24255543,24299875,24263796,24298084,24135310,24267564,24004644,24077241,24036948,24405024,24420756,24296354,24590921,24268142,23918597,24390374,24271464,23946420,24197450,24412897,24120820,24421894,23804281,24392403,24181174,24002022,24200839,24288442,24219713,23966208,24034168,24080738,24162920,24169501,24407665"
}
],
"service": "ECATCHER"
}
],
"visitorData": "CgtHV2dSNHFYMDhLSSjZ_4GcBg%3D%3D"
},
"trackingParams": "CAAQi24iEwim4-z298j7AhXH1BEIHUXtDAQ="
}

View file

@ -0,0 +1,59 @@
{
"responseContext": {
"serviceTrackingParams": [
{
"params": [
{
"key": "c",
"value": "WEB_REMIX"
},
{
"key": "cver",
"value": "1.20221121.01.00"
},
{
"key": "yt_li",
"value": "0"
},
{
"key": "GetMusicSearchSuggestions_rid",
"value": "0x0a90fd4a664adea1"
}
],
"service": "CSI"
},
{
"params": [
{
"key": "logged_in",
"value": "0"
},
{
"key": "e",
"value": "1714245,23804281,23882685,23918597,23934970,23946420,23966208,23983296,23998056,24001373,24002022,24002025,24004644,24007246,24034168,24036948,24077241,24080738,24120819,24135310,24140247,24161116,24162920,24164186,24169501,24181174,24184893,24187043,24187377,24191629,24197450,24199724,24200839,24211178,24217535,24219713,24241378,24255163,24255543,24255545,24262346,24263796,24265426,24267564,24268142,24279196,24280999,24281671,24282722,24287327,24288043,24290971,24292955,24293803,24299747,24299875,24390374,24390675,24391018,24391539,24392399,24400178,24400185,24401291,24401557,24402891,24404641,24406605,24407200,24407665,24412154,24413415,24414074,24414162,24415866,24416291,24416439,24417792,24418790,24420756,24421162,24423785,24426598,24426910,24590921,24591020,39322504,39322574"
}
],
"service": "GFEEDBACK"
},
{
"params": [
{
"key": "client.version",
"value": "1.20000101"
},
{
"key": "client.name",
"value": "WEB_REMIX"
},
{
"key": "client.fexp",
"value": "24292955,24413415,24077241,24001373,24263796,24267564,24161116,24290971,24412154,24191629,24420756,23882685,24299747,24288043,24591020,24184893,24414162,24255543,24406605,24200839,24287327,24280999,24407665,23946420,23804281,24390374,24140247,24414074,23966208,24080738,24418790,23983296,24219713,24164186,24187043,24400178,24400185,24404641,24426910,23998056,24391539,24007246,24401291,24293803,24135310,24255163,24268142,23918597,24187377,24004644,24390675,24255545,24036948,24423785,24392399,24417792,24299875,1714245,24217535,39322574,24281671,24002025,24426598,24199724,24401557,39322504,24421162,24120819,24211178,24265426,24402891,24391018,24181174,24002022,24416439,24407200,24034168,24415866,24282722,24241378,24162920,24416291,23934970,24169501,24590921,24279196,24262346,24197450"
}
],
"service": "ECATCHER"
}
],
"visitorData": "Cgtyb1QtdzRqZXZGayisj4KcBg%3D%3D"
},
"trackingParams": "CAAQi24iEwi6jvCx_8j7AhWbyxEIHYe1Bno="
}

View file

@ -1700,12 +1700,14 @@ async fn music_search_artists() {
let rp = RustyPipe::builder().strict().build();
let res = rp.query().music_search_artists("namika").await.unwrap();
let artist = res
let (i, artist) = res
.items
.items
.iter()
.find(|a| a.id == "UCIh4j8fXWf2U0ro0qnGU8Mg")
.enumerate()
.find(|(_, a)| a.id == "UCIh4j8fXWf2U0ro0qnGU8Mg")
.unwrap();
assert!(i < 3);
assert_eq!(artist.name, "Namika");
assert!(!artist.avatar.is_empty(), "got no avatar");
assert!(
@ -1741,9 +1743,15 @@ async fn music_search_playlists(#[case] with_community: bool) {
};
assert_eq!(res.corrected_query, None);
let playlist = &res.items.items[0];
let (i, playlist) = res
.items
.items
.iter()
.enumerate()
.find(|(_, p)| p.id == "RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk")
.unwrap();
assert_eq!(playlist.id, "RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk");
assert!(i < 3);
assert_eq!(playlist.name, "Easy Pop");
assert!(!playlist.thumbnail.is_empty(), "got no thumbnail");
assert_gte(playlist.track_count.unwrap(), 80, "tracks");
@ -1761,9 +1769,15 @@ async fn music_search_playlists_community() {
.unwrap();
assert_eq!(res.corrected_query, None);
let playlist = &res.items.items[0];
let (i, playlist) = res
.items
.items
.iter()
.enumerate()
.find(|(_, p)| p.id == "PLMC9KNkIncKtGvr2kFRuXBVmBev6cAJ2u")
.unwrap();
assert_eq!(playlist.id, "PLMC9KNkIncKtGvr2kFRuXBVmBev6cAJ2u");
assert!(i < 3);
assert_eq!(
playlist.name,
"Best Pop Music Videos - Top Pop Hits Playlist"
@ -1784,6 +1798,29 @@ async fn music_search_genre_radio() {
rp.query().music_search("pop radio").await.unwrap();
}
#[rstest]
#[case::default("ed sheer", Some("ed sheeran"))]
#[case::empty("reujbhevmfndxnjrze", None)]
#[tokio::test]
async fn music_search_suggestion(#[case] query: &str, #[case] expect: Option<&str>) {
let rp = RustyPipe::builder().strict().build();
let suggestion = rp.query().music_search_suggestion(query).await.unwrap();
match expect {
Some(expect) => assert!(
suggestion.iter().any(|s| s == expect),
"suggestion: {:?}, expected: {}",
suggestion,
expect
),
None => assert!(
suggestion.is_empty(),
"suggestion: {:?}, expected to be empty",
suggestion
),
}
}
#[rstest]
#[case::mv("mv", "ZeerrnuLi5E")]
#[case::track("track", "7nigXQS1Xb0")]