feat: add music search suggested items

This commit is contained in:
ThetaDev 2023-01-28 15:29:01 +01:00
parent 331a13568a
commit 1d56b9c9a0
8 changed files with 1016 additions and 150 deletions

View file

@ -7,7 +7,7 @@ use crate::{
error::{Error, ExtractionError}, error::{Error, ExtractionError},
model::{ model::{
paginator::Paginator, traits::FromYtItem, AlbumItem, ArtistItem, MusicPlaylistItem, paginator::Paginator, traits::FromYtItem, AlbumItem, ArtistItem, MusicPlaylistItem,
MusicSearchFiltered, MusicSearchResult, TrackItem, MusicSearchFiltered, MusicSearchResult, MusicSearchSuggestion, TrackItem,
}, },
serializer::MapResult, serializer::MapResult,
util::TryRemove, util::TryRemove,
@ -206,7 +206,7 @@ impl RustyPipeQuery {
pub async fn music_search_suggestion<S: AsRef<str>>( pub async fn music_search_suggestion<S: AsRef<str>>(
&self, &self,
query: S, query: S,
) -> Result<Vec<String>, Error> { ) -> Result<MusicSearchSuggestion, Error> {
let query = query.as_ref(); let query = query.as_ref();
let context = self.get_context(ClientType::DesktopMusic, true, None).await; let context = self.get_context(ClientType::DesktopMusic, true, None).await;
let request_body = QSearchSuggestion { let request_body = QSearchSuggestion {
@ -334,37 +334,40 @@ impl<T: FromYtItem> MapResponse<MusicSearchFiltered<T>> for response::MusicSearc
} }
} }
impl MapResponse<Vec<String>> for response::MusicSearchSuggestion { impl MapResponse<MusicSearchSuggestion> for response::MusicSearchSuggestion {
fn map_response( fn map_response(
self, self,
_id: &str, _id: &str,
_lang: crate::param::Language, lang: crate::param::Language,
_deobf: Option<&crate::deobfuscate::Deobfuscator>, _deobf: Option<&crate::deobfuscate::Deobfuscator>,
) -> Result<MapResult<Vec<String>>, ExtractionError> { ) -> Result<MapResult<MusicSearchSuggestion>, ExtractionError> {
let items = self let mut mapper = MusicListMapper::new(lang);
.contents let mut terms = Vec::new();
.into_iter()
.next() for section in self.contents {
.map(|content| { for item in section.search_suggestions_section_renderer.contents {
content match item {
.search_suggestions_section_renderer
.contents
.into_iter()
.filter_map(|itm| {
match itm {
response::music_search::SearchSuggestionItem::SearchSuggestionRenderer { response::music_search::SearchSuggestionItem::SearchSuggestionRenderer {
suggestion, suggestion,
} => Some(suggestion), } => {
response::music_search::SearchSuggestionItem::None => None, terms.push(suggestion);
},
response::music_search::SearchSuggestionItem::MusicResponsiveListItemRenderer(item) => {
mapper.add_response_item(response::music_item::MusicResponseItem::MusicResponsiveListItemRenderer(*item));
}
response::music_search::SearchSuggestionItem::None => {},
} }
}) }
.collect::<Vec<_>>() }
})
.unwrap_or_default(); let map_res = mapper.conv_items();
Ok(MapResult { Ok(MapResult {
c: items, c: MusicSearchSuggestion {
warnings: Vec::new(), terms,
items: map_res.c,
},
warnings: map_res.warnings,
}) })
} }
} }
@ -380,7 +383,7 @@ mod tests {
client::{response, MapResponse}, client::{response, MapResponse},
model::{ model::{
AlbumItem, ArtistItem, MusicPlaylistItem, MusicSearchFiltered, MusicSearchResult, AlbumItem, ArtistItem, MusicPlaylistItem, MusicSearchFiltered, MusicSearchResult,
TrackItem, MusicSearchSuggestion, TrackItem,
}, },
param::Language, param::Language,
serializer::MapResult, serializer::MapResult,
@ -499,7 +502,7 @@ mod tests {
let suggestion: response::MusicSearchSuggestion = let suggestion: response::MusicSearchSuggestion =
serde_json::from_reader(BufReader::new(json_file)).unwrap(); serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res: MapResult<Vec<String>> = let map_res: MapResult<MusicSearchSuggestion> =
suggestion.map_response("", Language::En, None).unwrap(); suggestion.map_response("", Language::En, None).unwrap();
assert!( assert!(

View file

@ -866,17 +866,13 @@ impl MusicListMapper {
) -> Option<MusicItemType> { ) -> Option<MusicItemType> {
let mut etype = None; let mut etype = None;
self.warnings.append(&mut res.warnings); self.warnings.append(&mut res.warnings);
res.c res.c.into_iter().for_each(|item| {
.into_iter() if let Some(et) = self.add_response_item(item) {
.for_each(|item| match self.map_item(item) { if etype.is_none() {
Ok(Some(et)) => { etype = Some(et);
if etype.is_none() {
etype = Some(et);
}
} }
Ok(None) => {} }
Err(e) => self.warnings.push(e), });
});
etype etype
} }
@ -884,6 +880,16 @@ impl MusicListMapper {
self.items.push(item); self.items.push(item);
} }
pub fn add_response_item(&mut self, item: MusicResponseItem) -> Option<MusicItemType> {
match self.map_item(item) {
Ok(et) => et,
Err(e) => {
self.warnings.push(e);
None
}
}
}
pub fn add_warnings(&mut self, warnings: &mut Vec<String>) { pub fn add_warnings(&mut self, warnings: &mut Vec<String>) {
self.warnings.append(warnings); self.warnings.append(warnings);
} }

View file

@ -3,7 +3,10 @@ use serde_with::{rust::deserialize_ignore_any, serde_as, VecSkipError};
use crate::serializer::text::Text; use crate::serializer::text::Text;
use super::{music_item::MusicShelf, ContentsRenderer, SectionList, Tab}; use super::{
music_item::{ListMusicItem, MusicShelf},
ContentsRenderer, SectionList, Tab,
};
/// Response model for YouTube Music search /// Response model for YouTube Music search
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -12,7 +15,7 @@ pub(crate) struct MusicSearch {
pub contents: Contents, pub contents: Contents,
} }
/// Response model for YouTube Music suggestion /// Response model for YouTube Music search suggestion
#[serde_as] #[serde_as]
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
@ -70,6 +73,7 @@ pub(crate) enum SearchSuggestionItem {
#[serde_as(as = "Text")] #[serde_as(as = "Text")]
suggestion: String, suggestion: String,
}, },
MusicResponsiveListItemRenderer(Box<ListMusicItem>),
#[serde(other, deserialize_with = "deserialize_ignore_any")] #[serde(other, deserialize_with = "deserialize_ignore_any")]
None, None,
} }

View file

@ -2,12 +2,76 @@
source: src/client/music_search.rs source: src/client/music_search.rs
expression: map_res.c expression: map_res.c
--- ---
[ MusicSearchSuggestion(
"taylor swift", terms: [
"tkkg", "taylor swift",
"techno", "tkkg",
"t low", "theo mach mir ein bananenbrot",
"the weeknd", "techno",
"tiktok songs", ],
"toten hosen", items: [
] Artist(ArtistItem(
id: "UCPC0L1d253x-KuMNwa05TpA",
name: "Taylor Swift",
avatar: [
Thumbnail(
url: "https://lh3.googleusercontent.com/U1cI80giSCUuNYx3zkRPt_AWytN1qFMlQoL5F7kTZeFzfIMmfHJYLJchX3BxeDLglE9MeVYp4OlN5Xc=w60-h60-p-l90-rj",
width: 60,
height: 60,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/U1cI80giSCUuNYx3zkRPt_AWytN1qFMlQoL5F7kTZeFzfIMmfHJYLJchX3BxeDLglE9MeVYp4OlN5Xc=w120-h120-p-l90-rj",
width: 120,
height: 120,
),
],
subscriber_count: None,
)),
Artist(ArtistItem(
id: "UCyiY-0Af0O6emoI3YvCEDaA",
name: "TKKG",
avatar: [
Thumbnail(
url: "https://lh3.googleusercontent.com/Y6iWyltVsuHYON5C7CvByIWYccxq_ZAw2UZiEMfYY4PlwzcNb54EmP3xHSFRn6ZWpLftvbXGTNkTchjq=w60-h60-l90-rj",
width: 60,
height: 60,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/Y6iWyltVsuHYON5C7CvByIWYccxq_ZAw2UZiEMfYY4PlwzcNb54EmP3xHSFRn6ZWpLftvbXGTNkTchjq=w120-h120-l90-rj",
width: 120,
height: 120,
),
],
subscriber_count: None,
)),
Track(TrackItem(
id: "0pnFvmuXwgg",
name: "Theo (Der Bananenbrot-Song)",
duration: None,
cover: [
Thumbnail(
url: "https://lh3.googleusercontent.com/x3Hn5hbqoPgf7D_JXotEAyUFTvdG_QwbfDqMqT-zdBgArAlqLlbYMN2FAWO5iwKkmcm-l_hUL4WtZd9u=w60-h60-s-l90-rj",
width: 60,
height: 60,
),
Thumbnail(
url: "https://lh3.googleusercontent.com/x3Hn5hbqoPgf7D_JXotEAyUFTvdG_QwbfDqMqT-zdBgArAlqLlbYMN2FAWO5iwKkmcm-l_hUL4WtZd9u=w120-h120-s-l90-rj",
width: 120,
height: 120,
),
],
artists: [
ArtistId(
id: Some("UC56hLMPuEsERdmTBbR_JGHA"),
name: "Rolf Zuckowski & seine Freunde",
),
],
artist_id: Some("UC56hLMPuEsERdmTBbR_JGHA"),
album: None,
view_count: None,
is_video: false,
track_nr: None,
by_va: false,
)),
],
)

View file

@ -2,4 +2,7 @@
source: src/client/music_search.rs source: src/client/music_search.rs
expression: map_res.c expression: map_res.c
--- ---
[] MusicSearchSuggestion(
terms: [],
items: [],
)

View file

@ -1278,3 +1278,13 @@ pub struct MusicGenreSection {
/// List of playlists of the genre section /// List of playlists of the genre section
pub playlists: Vec<MusicPlaylistItem>, pub playlists: Vec<MusicPlaylistItem>,
} }
/// YouTube Music suggested search terms/items
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct MusicSearchSuggestion {
/// Suggested search terms
pub terms: Vec<String>,
/// Suggested music items
pub items: Vec<MusicItem>,
}

File diff suppressed because it is too large Load diff

View file

@ -1421,8 +1421,8 @@ fn music_artist_not_found() {
fn music_search(#[case] typo: bool) { fn music_search(#[case] typo: bool) {
let rp = RustyPipe::builder().strict().build(); let rp = RustyPipe::builder().strict().build();
let res = tokio_test::block_on(rp.query().music_search(match typo { let res = tokio_test::block_on(rp.query().music_search(match typo {
false => "black mamba", false => "black mamba aespa",
true => "blck mamba", true => "blck mamba aespa",
})) }))
.unwrap(); .unwrap();
@ -1433,7 +1433,7 @@ fn music_search(#[case] typo: bool) {
assert_eq!(res.order[0], MusicItemType::Track); assert_eq!(res.order[0], MusicItemType::Track);
if typo { if typo {
assert_eq!(res.corrected_query.unwrap(), "black mamba"); assert_eq!(res.corrected_query.unwrap(), "black mamba aespa");
} else { } else {
assert_eq!(res.corrected_query, None); assert_eq!(res.corrected_query, None);
} }
@ -1700,22 +1700,33 @@ fn music_search_genre_radio() {
} }
#[rstest] #[rstest]
#[case::default("ed sheer", Some("ed sheeran"))] #[case::default("ed sheer", Some("ed sheeran"), Some("UClmXPfaYhXOYsNn_QUyheWQ"))]
#[case::empty("reujbhevmfndxnjrze", None)] #[case::empty("reujbhevmfndxnjrze", None, None)]
fn music_search_suggestion(#[case] query: &str, #[case] expect: Option<&str>) { fn music_search_suggestion(
#[case] query: &str,
#[case] term: Option<&str>,
#[case] artist: Option<&str>,
) {
let rp = RustyPipe::builder().strict().build(); let rp = RustyPipe::builder().strict().build();
let suggestion = tokio_test::block_on(rp.query().music_search_suggestion(query)).unwrap(); let suggestion = tokio_test::block_on(rp.query().music_search_suggestion(query)).unwrap();
match expect { match term {
Some(expect) => assert!( Some(expect) => assert!(
suggestion.iter().any(|s| s == expect), suggestion.terms.iter().any(|s| s == expect),
"suggestion: {suggestion:?}, expected: {expect}" "suggestion: {suggestion:?}, expected: {expect}"
), ),
None => assert!( None => assert!(
suggestion.is_empty(), suggestion.terms.is_empty(),
"suggestion: {suggestion:?}, expected to be empty" "suggestion: {suggestion:?}, expected to be empty"
), ),
} }
if let Some(artist) = artist {
assert!(suggestion.items.iter().any(|s| match s {
rustypipe::model::MusicItem::Artist(a) => a.id == artist,
_ => false,
}));
}
} }
#[rstest] #[rstest]