Skip to content

Commit ce63d94

Browse files
committed
merge: 'qq-engine' from memorydream's branch
2 parents 83069bc + f7bc2ad commit ce63d94

3 files changed

Lines changed: 266 additions & 0 deletions

File tree

Cargo.lock

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

engines/qq/Cargo.toml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
[package]
2+
name = "unm_engine_qq"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7+
8+
[dependencies]
9+
anyhow = "1.0.56"
10+
async-trait = "0.1.53"
11+
http = "0.2.6"
12+
serde_json = "1.0.79"
13+
serde = { version = "1.0.136", features = ["derive"] }
14+
reqwest = "0.11.10"
15+
futures = "0.3.21"
16+
url = "2.2.2"
17+
unm_engine = { version = "0.1.0", path = "../../engine-base" }
18+
unm_request = { version = "0.1.0", path = "../../request" }
19+
unm_selector = { version = "0.1.0", path = "../../selector" }
20+
unm_types = { version = "0.1.0", path = "../../types" }
21+
log = "0.4.16"
22+
23+
[dev-dependencies]
24+
tokio = { version = "1.17.0", features = ["macros", "rt-multi-thread"] }
25+
unm_test_utils = { path = "../../test-utils" }

engines/qq/src/lib.rs

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
use std::{borrow::Cow, collections::HashMap};
2+
3+
use async_trait::async_trait;
4+
use http::{Method, HeaderMap, HeaderValue, header::{ORIGIN, REFERER, COOKIE}};
5+
use log::debug;
6+
use reqwest::Url;
7+
use unm_engine::interface::Engine;
8+
use unm_selector::SimilarSongSelector;
9+
use unm_types::{Context, RetrievedSongInfo, SerializedIdentifier, Song, SongSearchInformation, Album};
10+
use unm_request::{json::{Json, UnableToExtractJson}, request};
11+
12+
pub const ENGINE_ID: &str = "qq";
13+
14+
pub struct QQEngine;
15+
16+
#[async_trait]
17+
impl Engine for QQEngine {
18+
async fn search<'a>(
19+
&self,
20+
info: &'a Song,
21+
ctx: &'a Context,
22+
) -> anyhow::Result<Option<SongSearchInformation<'static>>> {
23+
log::info!("Searching with QQ engine");
24+
25+
let response = get_search_data(&info.keyword(), ctx).await?;
26+
let result = response
27+
.pointer("/data/song/list")
28+
.ok_or_else(|| anyhow::anyhow!("/data/song/list not found"))?
29+
.as_array()
30+
.ok_or(UnableToExtractJson {
31+
json_pointer: "/data/song/list",
32+
expected_type: "array",
33+
})?;
34+
35+
let matched = find_match(info, result).await?;
36+
37+
if let Some(song) = matched {
38+
Ok(Some(SongSearchInformation {
39+
source: Cow::Borrowed(ENGINE_ID),
40+
identifier: song.id.to_string(),
41+
song: Some(song),
42+
}))
43+
} else {
44+
Ok(None)
45+
}
46+
}
47+
48+
49+
async fn retrieve<'a>(
50+
&self,
51+
identifier: &'a SerializedIdentifier,
52+
ctx: &'a Context,
53+
) -> anyhow::Result<RetrievedSongInfo<'static>> {
54+
todo!()
55+
}
56+
57+
}
58+
59+
async fn get_search_data(keyword: &str, ctx: &Context) -> anyhow::Result<Json> {
60+
debug!("Getting the search data from QQ Music…");
61+
62+
let url = construct_search_url(keyword)?;
63+
let cookie = get_cookie(ctx);
64+
let res = request(
65+
Method::GET,
66+
&url,
67+
Some(construct_header(cookie)?),
68+
None,
69+
ctx.try_get_proxy()?
70+
)
71+
.await?;
72+
Ok(res.json().await?)
73+
}
74+
75+
fn get_cookie(context: &Context) -> Option<&str> {
76+
if let Some(ref config) = context.config {
77+
config.get_deref(Cow::Borrowed("qq:cookie"))
78+
} else {
79+
None
80+
}
81+
}
82+
83+
async fn find_match(info: &Song, data: &[Json]) -> anyhow::Result<Option<Song>> {
84+
let selector = SimilarSongSelector::new(info).optional_selector;
85+
let similar_song = data
86+
.iter()
87+
.map(|entry| format(entry).ok())
88+
.find(|s| selector(&s))
89+
.expect("shoule be Some");
90+
91+
Ok(similar_song)
92+
}
93+
94+
fn format(song: &Json) -> anyhow::Result<Song> {
95+
debug!("Formatting the response to Song…");
96+
97+
let id = song["songid"].as_i64().ok_or(UnableToExtractJson {
98+
json_pointer: "/songid",
99+
expected_type: "i64",
100+
})?;
101+
let name = song["songname"].as_str().ok_or(UnableToExtractJson {
102+
json_pointer: "/songname",
103+
expected_type: "string",
104+
})?;
105+
let duration = song["interval"].as_i64().ok_or(UnableToExtractJson {
106+
json_pointer: "/interval",
107+
expected_type: "i64",
108+
})?;
109+
let album_id = song["albumid"].as_i64().ok_or(UnableToExtractJson {
110+
json_pointer: "/albumid",
111+
expected_type: "i64",
112+
})?;
113+
let album_name = song["albumname"].as_str().ok_or(UnableToExtractJson {
114+
json_pointer: "/albumname",
115+
expected_type: "string",
116+
})?;
117+
118+
let media_mid = song["media_mid"].as_str().ok_or(UnableToExtractJson {
119+
json_pointer: "/media_mid",
120+
expected_type: "string",
121+
})?;
122+
let song_mid = song["songmid"].as_str().ok_or(UnableToExtractJson {
123+
json_pointer: "/songmid",
124+
expected_type: "string",
125+
})?;
126+
let mut context = HashMap::new();
127+
context.insert("media_mid".to_string(), media_mid.to_string());
128+
context.insert("songmid".to_string(), song_mid.to_string());
129+
130+
let x = Song {
131+
id: id.to_string(),
132+
name: String::from(name),
133+
duration: Some(duration * 1000),
134+
artists: vec![],
135+
album: Some(Album {
136+
id: album_id.to_string(),
137+
name: String::from(album_name),
138+
}),
139+
context: Some(context),
140+
};
141+
142+
Ok(x)
143+
}
144+
145+
fn construct_header(cookie: Option<&str>) -> anyhow::Result<HeaderMap> {
146+
log::debug!("Constructing the header for QQ Music…");
147+
148+
let mut hm = HeaderMap::new();
149+
150+
hm.insert(ORIGIN, HeaderValue::from_static("http://y.qq.com"));
151+
hm.insert(REFERER, HeaderValue::from_static("http://y.qq.com"));
152+
153+
if let Some(cookie) = cookie {
154+
hm.insert(COOKIE, HeaderValue::from_str(cookie)?);
155+
}
156+
157+
Ok(hm)
158+
}
159+
160+
fn construct_search_url(keyword: &str) -> anyhow::Result<Url> {
161+
Ok(Url::parse_with_params(
162+
"https://c.y.qq.com/soso/fcgi-bin/client_search_cp?",
163+
&[
164+
("ct", "24"),
165+
("qqmusic_ver", "1298"),
166+
("remoteplace", "txt.yqq.center"),
167+
("t", "0"),
168+
("aggr", "1"),
169+
("cr", "1"),
170+
("catZhida", "1"),
171+
("lossless", "1"),
172+
("flag_qc", "0"),
173+
("p", "1"),
174+
("n", "20"),
175+
("w", keyword),
176+
("g_tk", "5381"),
177+
("loginUin", "0"),
178+
("hostUin", "0"),
179+
("format", "json"),
180+
("inCharset", "utf8"),
181+
("outCharset", "utf-8"),
182+
("notice", "0"),
183+
("platform", "yqq"),
184+
("needNewCode", "0")
185+
]
186+
)?)
187+
}
188+
189+
#[cfg(test)]
190+
mod tests {
191+
use tokio::test;
192+
use unm_types::{Context, Artist};
193+
194+
use super::*;
195+
196+
fn get_info_1() -> Song {
197+
// https://music.163.com/api/song/detail?ids=[385552]
198+
Song {
199+
name: String::from("干杯"),
200+
artists: vec![Artist {
201+
name: String::from("五月天"),
202+
..Default::default()
203+
}],
204+
..Default::default()
205+
}
206+
}
207+
208+
#[test]
209+
async fn qq_search() {
210+
let info = get_info_1();
211+
let info = QQEngine
212+
.search(&info, &Context::default())
213+
.await
214+
.unwrap()
215+
.unwrap();
216+
217+
assert_eq!(info.identifier, "1056382");
218+
assert_eq!(info.source, ENGINE_ID);
219+
}
220+
}

0 commit comments

Comments
 (0)