feat: add music search suggested items
This commit is contained in:
parent
331a13568a
commit
1d56b9c9a0
8 changed files with 1016 additions and 150 deletions
|
|
@ -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!(
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)),
|
||||
],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,4 +2,7 @@
|
|||
source: src/client/music_search.rs
|
||||
expression: map_res.c
|
||||
---
|
||||
[]
|
||||
MusicSearchSuggestion(
|
||||
terms: [],
|
||||
items: [],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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]
|
||||
|
|
|
|||
Reference in a new issue