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},
model::{
paginator::Paginator, traits::FromYtItem, AlbumItem, ArtistItem, MusicPlaylistItem,
MusicSearchFiltered, MusicSearchResult, TrackItem,
MusicSearchFiltered, MusicSearchResult, MusicSearchSuggestion, TrackItem,
},
serializer::MapResult,
util::TryRemove,
@ -206,7 +206,7 @@ impl RustyPipeQuery {
pub async fn music_search_suggestion<S: AsRef<str>>(
&self,
query: S,
) -> Result<Vec<String>, Error> {
) -> Result<MusicSearchSuggestion, Error> {
let query = query.as_ref();
let context = self.get_context(ClientType::DesktopMusic, true, None).await;
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(
self,
_id: &str,
_lang: crate::param::Language,
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 {
) -> Result<MapResult<MusicSearchSuggestion>, ExtractionError> {
let mut mapper = MusicListMapper::new(lang);
let mut terms = Vec::new();
for section in self.contents {
for item in section.search_suggestions_section_renderer.contents {
match item {
response::music_search::SearchSuggestionItem::SearchSuggestionRenderer {
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 {
c: items,
warnings: Vec::new(),
c: MusicSearchSuggestion {
terms,
items: map_res.c,
},
warnings: map_res.warnings,
})
}
}
@ -380,7 +383,7 @@ mod tests {
client::{response, MapResponse},
model::{
AlbumItem, ArtistItem, MusicPlaylistItem, MusicSearchFiltered, MusicSearchResult,
TrackItem,
MusicSearchSuggestion, TrackItem,
},
param::Language,
serializer::MapResult,
@ -499,7 +502,7 @@ mod tests {
let suggestion: response::MusicSearchSuggestion =
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();
assert!(

View file

@ -866,17 +866,13 @@ impl MusicListMapper {
) -> Option<MusicItemType> {
let mut etype = None;
self.warnings.append(&mut res.warnings);
res.c
.into_iter()
.for_each(|item| match self.map_item(item) {
Ok(Some(et)) => {
if etype.is_none() {
etype = Some(et);
}
res.c.into_iter().for_each(|item| {
if let Some(et) = self.add_response_item(item) {
if etype.is_none() {
etype = Some(et);
}
Ok(None) => {}
Err(e) => self.warnings.push(e),
});
}
});
etype
}
@ -884,6 +880,16 @@ impl MusicListMapper {
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>) {
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 super::{music_item::MusicShelf, ContentsRenderer, SectionList, Tab};
use super::{
music_item::{ListMusicItem, MusicShelf},
ContentsRenderer, SectionList, Tab,
};
/// Response model for YouTube Music search
#[derive(Debug, Deserialize)]
@ -12,7 +15,7 @@ pub(crate) struct MusicSearch {
pub contents: Contents,
}
/// Response model for YouTube Music suggestion
/// Response model for YouTube Music search suggestion
#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
@ -70,6 +73,7 @@ pub(crate) enum SearchSuggestionItem {
#[serde_as(as = "Text")]
suggestion: String,
},
MusicResponsiveListItemRenderer(Box<ListMusicItem>),
#[serde(other, deserialize_with = "deserialize_ignore_any")]
None,
}

View file

@ -2,12 +2,76 @@
source: src/client/music_search.rs
expression: map_res.c
---
[
"taylor swift",
"tkkg",
"techno",
"t low",
"the weeknd",
"tiktok songs",
"toten hosen",
]
MusicSearchSuggestion(
terms: [
"taylor swift",
"tkkg",
"theo mach mir ein bananenbrot",
"techno",
],
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
expression: map_res.c
---
[]
MusicSearchSuggestion(
terms: [],
items: [],
)

View file

@ -1278,3 +1278,13 @@ pub struct MusicGenreSection {
/// List of playlists of the genre section
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) {
let rp = RustyPipe::builder().strict().build();
let res = tokio_test::block_on(rp.query().music_search(match typo {
false => "black mamba",
true => "blck mamba",
false => "black mamba aespa",
true => "blck mamba aespa",
}))
.unwrap();
@ -1433,7 +1433,7 @@ fn music_search(#[case] typo: bool) {
assert_eq!(res.order[0], MusicItemType::Track);
if typo {
assert_eq!(res.corrected_query.unwrap(), "black mamba");
assert_eq!(res.corrected_query.unwrap(), "black mamba aespa");
} else {
assert_eq!(res.corrected_query, None);
}
@ -1700,22 +1700,33 @@ fn music_search_genre_radio() {
}
#[rstest]
#[case::default("ed sheer", Some("ed sheeran"))]
#[case::empty("reujbhevmfndxnjrze", None)]
fn music_search_suggestion(#[case] query: &str, #[case] expect: Option<&str>) {
#[case::default("ed sheer", Some("ed sheeran"), Some("UClmXPfaYhXOYsNn_QUyheWQ"))]
#[case::empty("reujbhevmfndxnjrze", None, None)]
fn music_search_suggestion(
#[case] query: &str,
#[case] term: Option<&str>,
#[case] artist: Option<&str>,
) {
let rp = RustyPipe::builder().strict().build();
let suggestion = tokio_test::block_on(rp.query().music_search_suggestion(query)).unwrap();
match expect {
match term {
Some(expect) => assert!(
suggestion.iter().any(|s| s == expect),
suggestion.terms.iter().any(|s| s == expect),
"suggestion: {suggestion:?}, expected: {expect}"
),
None => assert!(
suggestion.is_empty(),
suggestion.terms.is_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]