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},
|
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!(
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -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: [],
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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]
|
||||||
|
|
|
||||||
Reference in a new issue