feat: music search suggestions

This commit is contained in:
ThetaDev 2022-11-25 10:33:49 +01:00
parent ef86181627
commit bd936a8c42
10 changed files with 474 additions and 3 deletions

View file

@ -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**

View file

@ -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"),

View file

@ -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};
@ -417,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);
}
} }

View file

@ -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;

View file

@ -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,
}

View file

@ -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",
]

View file

@ -0,0 +1,5 @@
---
source: src/client/music_search.rs
expression: map_res.c
---
[]

View 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="
}

View 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="
}

View file

@ -1798,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")]