Compare commits
3 commits
fe8ff37f66
...
bd936a8c42
Author | SHA1 | Date | |
---|---|---|---|
bd936a8c42 | |||
ef86181627 | |||
fc8bce43fd |
18 changed files with 9193 additions and 209 deletions
|
@ -23,7 +23,7 @@ inspired by [NewPipe](https://github.com/TeamNewPipe/NewPipeExtractor).
|
||||||
- [X] **Album**
|
- [X] **Album**
|
||||||
- [X] **Artist**
|
- [X] **Artist**
|
||||||
- [X] **Search**
|
- [X] **Search**
|
||||||
- [ ] **Search suggestions**
|
- [X] **Search suggestions**
|
||||||
- [X] **Radio**
|
- [X] **Radio**
|
||||||
- [X] **Track details** (lyrics, recommendations)
|
- [X] **Track details** (lyrics, recommendations)
|
||||||
- [ ] **Moods**
|
- [ ] **Moods**
|
||||||
|
|
|
@ -48,6 +48,7 @@ pub async fn download_testfiles(project_root: &Path) {
|
||||||
music_search_artists(&testfiles).await;
|
music_search_artists(&testfiles).await;
|
||||||
music_search_playlists(&testfiles).await;
|
music_search_playlists(&testfiles).await;
|
||||||
music_search_cont(&testfiles).await;
|
music_search_cont(&testfiles).await;
|
||||||
|
music_search_suggestion(&testfiles).await;
|
||||||
music_artist(&testfiles).await;
|
music_artist(&testfiles).await;
|
||||||
music_details(&testfiles).await;
|
music_details(&testfiles).await;
|
||||||
music_lyrics(&testfiles).await;
|
music_lyrics(&testfiles).await;
|
||||||
|
@ -585,10 +586,14 @@ async fn music_album(testfiles: &Path) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn music_search(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();
|
let mut json_path = testfiles.to_path_buf();
|
||||||
json_path.push("music_search");
|
json_path.push("music_search");
|
||||||
json_path.push(format!("{}.json", name));
|
json_path.push(format!("main_{}.json", name));
|
||||||
if json_path.exists() {
|
if json_path.exists() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -684,6 +689,20 @@ async fn music_search_cont(testfiles: &Path) {
|
||||||
res.items.next(&rp.query()).await.unwrap().unwrap();
|
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) {
|
async fn music_artist(testfiles: &Path) {
|
||||||
for (name, id) in [
|
for (name, id) in [
|
||||||
("default", "UClmXPfaYhXOYsNn_QUyheWQ"),
|
("default", "UClmXPfaYhXOYsNn_QUyheWQ"),
|
||||||
|
|
|
@ -24,6 +24,13 @@ struct QSearch<'a> {
|
||||||
params: Option<Params>,
|
params: Option<Params>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct QSearchSuggestion<'a> {
|
||||||
|
context: YTContext<'a>,
|
||||||
|
input: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
enum Params {
|
enum Params {
|
||||||
#[serde(rename = "EgWKAQIIAWoMEAMQBBAJEA4QChAF")]
|
#[serde(rename = "EgWKAQIIAWoMEAMQBBAJEA4QChAF")]
|
||||||
|
@ -182,6 +189,23 @@ impl RustyPipeQuery {
|
||||||
)
|
)
|
||||||
.await
|
.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 {
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::{fs::File, io::BufReader, path::Path};
|
use std::{fs::File, io::BufReader, path::Path};
|
||||||
|
@ -312,8 +371,9 @@ mod tests {
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[case::default("default")]
|
#[case::default("default")]
|
||||||
#[case::typo("typo")]
|
#[case::typo("typo")]
|
||||||
fn map_music_search(#[case] name: &str) {
|
#[case::radio("radio")]
|
||||||
let filename = format!("testfiles/music_search/{}.json", name);
|
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_path = Path::new(&filename);
|
||||||
let json_file = File::open(json_path).unwrap();
|
let json_file = File::open(json_path).unwrap();
|
||||||
|
|
||||||
|
@ -328,7 +388,7 @@ mod tests {
|
||||||
map_res.warnings
|
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]
|
#[rstest]
|
||||||
|
@ -416,4 +476,26 @@ mod tests {
|
||||||
|
|
||||||
insta::assert_ron_snapshot!(format!("map_music_search_playlists_{}", name), map_res.c);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ pub(crate) use music_details::MusicRelated;
|
||||||
pub(crate) use music_item::MusicContinuation;
|
pub(crate) use music_item::MusicContinuation;
|
||||||
pub(crate) use music_playlist::MusicPlaylist;
|
pub(crate) use music_playlist::MusicPlaylist;
|
||||||
pub(crate) use music_search::MusicSearch;
|
pub(crate) use music_search::MusicSearch;
|
||||||
|
pub(crate) use music_search::MusicSearchSuggestion;
|
||||||
pub(crate) use player::Player;
|
pub(crate) use player::Player;
|
||||||
pub(crate) use playlist::Playlist;
|
pub(crate) use playlist::Playlist;
|
||||||
pub(crate) use playlist::PlaylistCont;
|
pub(crate) use playlist::PlaylistCont;
|
||||||
|
|
|
@ -15,7 +15,7 @@ use crate::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
url_endpoint::{BrowseEndpointWrap, NavigationEndpoint, PageType},
|
url_endpoint::{BrowseEndpointWrap, MusicPageType, NavigationEndpoint, PageType},
|
||||||
ContentsRenderer, MusicContinuationData, Thumbnails, ThumbnailsWrap,
|
ContentsRenderer, MusicContinuationData, Thumbnails, ThumbnailsWrap,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -408,119 +408,30 @@ impl MusicListMapper {
|
||||||
let c2 = columns.next();
|
let c2 = columns.next();
|
||||||
let c3 = columns.next();
|
let c3 = columns.next();
|
||||||
|
|
||||||
match item.navigation_endpoint {
|
let first_tn = item
|
||||||
// Artist / Album / Playlist
|
.thumbnail
|
||||||
Some(ne) => {
|
.music_thumbnail_renderer
|
||||||
let mut subtitle_parts = c2
|
.thumbnail
|
||||||
.ok_or_else(|| "could not get subtitle".to_owned())?
|
.thumbnails
|
||||||
.renderer
|
.first();
|
||||||
.text
|
|
||||||
.split(util::DOT_SEPARATOR)
|
|
||||||
.into_iter();
|
|
||||||
|
|
||||||
let (page_type, id) = match ne.music_page() {
|
let pt_id = item
|
||||||
Some(music_page) => music_page,
|
.navigation_endpoint
|
||||||
None => {
|
.and_then(|ne| ne.music_page())
|
||||||
// Ignore radio items
|
.or_else(|| {
|
||||||
if subtitle_parts.len() == 1 {
|
item.playlist_item_data
|
||||||
return Ok(None);
|
.map(|d| (MusicPageType::Track, d.video_id))
|
||||||
}
|
})
|
||||||
return Err("invalid navigation endpoint".to_string());
|
.or_else(|| {
|
||||||
}
|
first_tn.and_then(|tn| {
|
||||||
};
|
util::video_id_from_thumbnail_url(&tn.url)
|
||||||
|
.map(|id| (MusicPageType::Track, id))
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
let title =
|
match pt_id {
|
||||||
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
|
// Track
|
||||||
None => {
|
Some((MusicPageType::Track, id)) => {
|
||||||
let first_tn = item
|
|
||||||
.thumbnail
|
|
||||||
.music_thumbnail_renderer
|
|
||||||
.thumbnail
|
|
||||||
.thumbnails
|
|
||||||
.first();
|
|
||||||
|
|
||||||
let id = item
|
|
||||||
.playlist_item_data
|
|
||||||
.map(|d| d.video_id)
|
|
||||||
.or_else(|| {
|
|
||||||
first_tn.and_then(|tn| util::video_id_from_thumbnail_url(&tn.url))
|
|
||||||
})
|
|
||||||
.ok_or_else(|| "no video id".to_owned())?;
|
|
||||||
|
|
||||||
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))?;
|
||||||
|
|
||||||
|
@ -633,6 +544,92 @@ impl MusicListMapper {
|
||||||
}));
|
}));
|
||||||
Ok(Some(MusicEntityType::Track))
|
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
|
// Tile
|
||||||
|
@ -642,44 +639,45 @@ impl MusicListMapper {
|
||||||
let subtitle_p2 = subtitle_parts.next();
|
let subtitle_p2 = subtitle_parts.next();
|
||||||
let subtitle_p3 = subtitle_parts.next();
|
let subtitle_p3 = subtitle_parts.next();
|
||||||
|
|
||||||
match item.navigation_endpoint.watch_endpoint {
|
match item.navigation_endpoint.music_page() {
|
||||||
// Music video
|
Some((page_type, id)) => match page_type {
|
||||||
Some(wep) => {
|
MusicPageType::Track => {
|
||||||
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 {
|
||||||
id: wep.video_id,
|
id,
|
||||||
title: item.title,
|
title: item.title,
|
||||||
duration: None,
|
duration: None,
|
||||||
cover: item.thumbnail_renderer.into(),
|
cover: item.thumbnail_renderer.into(),
|
||||||
artist_id: artists.first().and_then(|a| a.id.to_owned()),
|
artist_id: artists.first().and_then(|a| a.id.to_owned()),
|
||||||
artists,
|
artists,
|
||||||
album: None,
|
album: None,
|
||||||
view_count: subtitle_p2
|
view_count: subtitle_p2.and_then(|c| {
|
||||||
.and_then(|c| util::parse_large_numstr(c.first_str(), self.lang)),
|
util::parse_large_numstr(c.first_str(), self.lang)
|
||||||
is_video: true,
|
}),
|
||||||
track_nr: None,
|
is_video: true,
|
||||||
}));
|
track_nr: None,
|
||||||
Ok(Some(MusicEntityType::Track))
|
}));
|
||||||
}
|
Ok(Some(MusicEntityType::Track))
|
||||||
// Artist / Album / Playlist
|
}
|
||||||
None => {
|
MusicPageType::Artist => {
|
||||||
let (page_type, id) = item
|
let subscriber_count = subtitle_p1
|
||||||
.navigation_endpoint
|
.and_then(|p| util::parse_large_numstr(p.first_str(), self.lang));
|
||||||
.music_page()
|
|
||||||
.ok_or_else(|| "could not get navigation endpoint".to_owned())?;
|
|
||||||
|
|
||||||
match page_type {
|
self.items.push(MusicItem::Artist(ArtistItem {
|
||||||
PageType::Album => {
|
id,
|
||||||
let mut year = None;
|
name: item.title,
|
||||||
let mut album_type = AlbumType::Single;
|
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 (
|
let (artists, by_va) =
|
||||||
subtitle_p1,
|
match (subtitle_p1, subtitle_p2, &self.artists, self.artist_page) {
|
||||||
subtitle_p2,
|
|
||||||
&self.artists,
|
|
||||||
self.artist_page,
|
|
||||||
) {
|
|
||||||
// "2022" (Artist singles)
|
// "2022" (Artist singles)
|
||||||
(Some(year_txt), None, Some(artists), true) => {
|
(Some(year_txt), None, Some(artists), true) => {
|
||||||
year = util::parse_numeric(year_txt.first_str()).ok();
|
year = util::parse_numeric(year_txt.first_str()).ok();
|
||||||
|
@ -706,56 +704,41 @@ impl MusicListMapper {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
self.items.push(MusicItem::Album(AlbumItem {
|
self.items.push(MusicItem::Album(AlbumItem {
|
||||||
id,
|
id,
|
||||||
name: item.title,
|
name: item.title,
|
||||||
cover: item.thumbnail_renderer.into(),
|
cover: item.thumbnail_renderer.into(),
|
||||||
artists,
|
artists,
|
||||||
album_type,
|
album_type,
|
||||||
year,
|
year,
|
||||||
by_va,
|
by_va,
|
||||||
}));
|
}));
|
||||||
Ok(Some(MusicEntityType::Album))
|
Ok(Some(MusicEntityType::Album))
|
||||||
}
|
|
||||||
PageType::Playlist => {
|
|
||||||
let from_ytm = subtitle_p2
|
|
||||||
.as_ref()
|
|
||||||
.map(|p| p.first_str() == util::YT_MUSIC_NAME)
|
|
||||||
.unwrap_or_default();
|
|
||||||
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());
|
|
||||||
|
|
||||||
self.items.push(MusicItem::Playlist(MusicPlaylistItem {
|
|
||||||
id,
|
|
||||||
name: item.title,
|
|
||||||
thumbnail: item.thumbnail_renderer.into(),
|
|
||||||
channel,
|
|
||||||
track_count,
|
|
||||||
from_ytm,
|
|
||||||
}));
|
|
||||||
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::Playlist => {
|
||||||
|
let from_ytm = subtitle_p2
|
||||||
|
.as_ref()
|
||||||
|
.map(|p| p.first_str() == util::YT_MUSIC_NAME)
|
||||||
|
.unwrap_or_default();
|
||||||
|
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());
|
||||||
|
|
||||||
|
self.items.push(MusicItem::Playlist(MusicPlaylistItem {
|
||||||
|
id,
|
||||||
|
name: item.title,
|
||||||
|
thumbnail: item.thumbnail_renderer.into(),
|
||||||
|
channel,
|
||||||
|
track_count,
|
||||||
|
from_ytm,
|
||||||
|
}));
|
||||||
|
Ok(Some(MusicEntityType::Playlist))
|
||||||
|
}
|
||||||
|
MusicPageType::None => Ok(None),
|
||||||
|
},
|
||||||
|
None => Err("could not determine item type".to_owned()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,16 @@ pub(crate) struct MusicSearch {
|
||||||
pub contents: Contents,
|
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)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub(crate) struct Contents {
|
pub(crate) struct Contents {
|
||||||
|
@ -45,3 +55,21 @@ pub(crate) struct ShowingResultsForRenderer {
|
||||||
#[serde_as(as = "Text")]
|
#[serde_as(as = "Text")]
|
||||||
pub corrected_query: String,
|
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,
|
||||||
|
}
|
||||||
|
|
|
@ -32,6 +32,7 @@ pub(crate) struct NavigationEndpoint {
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub(crate) struct WatchEndpoint {
|
pub(crate) struct WatchEndpoint {
|
||||||
pub video_id: String,
|
pub video_id: String,
|
||||||
|
pub playlist_id: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub start_time_seconds: u32,
|
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 {
|
impl NavigationEndpoint {
|
||||||
pub(crate) fn music_page(self) -> Option<(PageType, String)> {
|
pub(crate) fn music_page(self) -> Option<(MusicPageType, String)> {
|
||||||
match self.browse_endpoint {
|
match self.browse_endpoint {
|
||||||
Some(browse) => match browse.browse_endpoint_context_supported_configs {
|
Some(browse) => match browse.browse_endpoint_context_supported_configs {
|
||||||
Some(config) => Some((
|
Some(config) => Some((
|
||||||
config.browse_endpoint_context_music_config.page_type,
|
config.browse_endpoint_context_music_config.page_type.into(),
|
||||||
browse.browse_id,
|
browse.browse_id,
|
||||||
)),
|
)),
|
||||||
None => None,
|
None => None,
|
||||||
},
|
},
|
||||||
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
],
|
||||||
|
)
|
|
@ -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",
|
||||||
|
]
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
source: src/client/music_search.rs
|
||||||
|
expression: map_res.c
|
||||||
|
---
|
||||||
|
[]
|
7983
testfiles/music_search/main_radio.json
Normal file
7983
testfiles/music_search/main_radio.json
Normal file
File diff suppressed because it is too large
Load diff
242
testfiles/music_search/suggestion_default.json
Normal file
242
testfiles/music_search/suggestion_default.json
Normal 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="
|
||||||
|
}
|
59
testfiles/music_search/suggestion_empty.json
Normal file
59
testfiles/music_search/suggestion_empty.json
Normal 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="
|
||||||
|
}
|
|
@ -1700,12 +1700,14 @@ async fn music_search_artists() {
|
||||||
let rp = RustyPipe::builder().strict().build();
|
let rp = RustyPipe::builder().strict().build();
|
||||||
let res = rp.query().music_search_artists("namika").await.unwrap();
|
let res = rp.query().music_search_artists("namika").await.unwrap();
|
||||||
|
|
||||||
let artist = res
|
let (i, artist) = res
|
||||||
.items
|
.items
|
||||||
.items
|
.items
|
||||||
.iter()
|
.iter()
|
||||||
.find(|a| a.id == "UCIh4j8fXWf2U0ro0qnGU8Mg")
|
.enumerate()
|
||||||
|
.find(|(_, a)| a.id == "UCIh4j8fXWf2U0ro0qnGU8Mg")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
assert!(i < 3);
|
||||||
assert_eq!(artist.name, "Namika");
|
assert_eq!(artist.name, "Namika");
|
||||||
assert!(!artist.avatar.is_empty(), "got no avatar");
|
assert!(!artist.avatar.is_empty(), "got no avatar");
|
||||||
assert!(
|
assert!(
|
||||||
|
@ -1741,9 +1743,15 @@ async fn music_search_playlists(#[case] with_community: bool) {
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(res.corrected_query, None);
|
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_eq!(playlist.name, "Easy Pop");
|
||||||
assert!(!playlist.thumbnail.is_empty(), "got no thumbnail");
|
assert!(!playlist.thumbnail.is_empty(), "got no thumbnail");
|
||||||
assert_gte(playlist.track_count.unwrap(), 80, "tracks");
|
assert_gte(playlist.track_count.unwrap(), 80, "tracks");
|
||||||
|
@ -1761,9 +1769,15 @@ async fn music_search_playlists_community() {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(res.corrected_query, None);
|
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!(
|
assert_eq!(
|
||||||
playlist.name,
|
playlist.name,
|
||||||
"Best Pop Music Videos - Top Pop Hits Playlist"
|
"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();
|
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]
|
#[rstest]
|
||||||
#[case::mv("mv", "ZeerrnuLi5E")]
|
#[case::mv("mv", "ZeerrnuLi5E")]
|
||||||
#[case::track("track", "7nigXQS1Xb0")]
|
#[case::track("track", "7nigXQS1Xb0")]
|
||||||
|
|
Loading…
Reference in a new issue