1use std::error::Error;
2use std::ops::Deref;
3
4use reqwest::Url;
5use serde::de::DeserializeOwned;
6use serde::{Deserialize, Serialize};
7
8use crate::actress_search::{ActressSearchParams, ActressSearchResult};
9use crate::author_search::{AuthorSearchParams, AuthorSearchResult};
10use crate::dmm::ApiParams::{
11 ActressSearch, AuthorSearch, FloorList, GenreSearch, ItemList, MakerSearch, SeriesSearch,
12};
13use crate::floor_list::{FloorListParams, FloorListResult};
14use crate::genre_search::{GenreSearchParams, GenreSearchResult};
15use crate::item_list::{ItemListParams, ItemListResult};
16use crate::maker_search::{MakerSearchParams, MakerSearchResult};
17use crate::series_search::{SeriesSearchParams, SeriesSearchResult};
18
19const ENDPOINT_BASE: &str = "https://api.dmm.com/affiliate/v3/";
20
21#[derive(Serialize, Debug)]
22#[serde(untagged)]
23#[allow(clippy::large_enum_variant)]
24enum ApiParams {
25 ItemList(ItemListParams),
26 FloorList(FloorListParams),
27 ActressSearch(ActressSearchParams),
28 GenreSearch(GenreSearchParams),
29 MakerSearch(MakerSearchParams),
30 SeriesSearch(SeriesSearchParams),
31 AuthorSearch(AuthorSearchParams),
32}
33
34#[derive(Deserialize, Debug)]
35struct ApiResponse<T: ApiResult> {
36 pub result: T,
37}
38
39pub(crate) trait ApiResult {}
40
41#[derive(Deserialize, Debug)]
42pub struct ElementVec<T> {
43 #[serde(rename = "item")]
44 pub items: Vec<T>,
45}
46
47impl<T> Deref for ElementVec<T> {
48 type Target = Vec<T>;
49
50 fn deref(&self) -> &Self::Target {
51 &self.items
52 }
53}
54pub struct Dmm {
55 api_id: String,
56 affiliate_id: String,
57}
58
59impl Dmm {
60 pub fn new(dmm_api_id: &str, dmm_affiliate_id: &str) -> Self {
61 Dmm {
62 api_id: dmm_api_id.to_string(),
63 affiliate_id: dmm_affiliate_id.to_string(),
64 }
65 }
66
67 async fn call<T>(&self, api: &str, params: ApiParams) -> Result<T, Box<dyn Error>>
68 where
69 T: ApiResult + DeserializeOwned,
70 {
71 let auth = querystring::stringify(vec![
72 ("api_id", &self.api_id),
73 ("affiliate_id", &self.affiliate_id),
74 ("output", "xml"),
75 ]);
76 let qs = serde_qs::to_string(¶ms)?;
77 let mut url = Url::parse(ENDPOINT_BASE)?.join(api)?;
79 url.set_query(Some(&(auth + &qs)));
80 let res = reqwest::get(url).await?;
81 let text = res.text().await?;
82 let res: ApiResponse<T> = serde_xml_rs::from_str(&text)?;
84 Ok(res.result)
85 }
86
87 pub async fn item_list(
88 &self,
89 params: ItemListParams,
90 ) -> Result<ItemListResult, Box<dyn Error>> {
91 self.call("ItemList", ItemList(params)).await
92 }
93
94 pub async fn floor_list(
95 &self,
96 params: FloorListParams,
97 ) -> Result<FloorListResult, Box<dyn Error>> {
98 self.call("FloorList", FloorList(params)).await
99 }
100
101 pub async fn actress_search(
102 &self,
103 params: ActressSearchParams,
104 ) -> Result<ActressSearchResult, Box<dyn Error>> {
105 self.call("ActressSearch", ActressSearch(params)).await
106 }
107
108 pub async fn genre_search(
109 &self,
110 params: GenreSearchParams,
111 ) -> Result<GenreSearchResult, Box<dyn Error>> {
112 self.call("GenreSearch", GenreSearch(params)).await
113 }
114
115 pub async fn maker_search(
116 &self,
117 params: MakerSearchParams,
118 ) -> Result<MakerSearchResult, Box<dyn Error>> {
119 self.call("MakerSearch", MakerSearch(params)).await
120 }
121
122 pub async fn series_search(
123 &self,
124 params: SeriesSearchParams,
125 ) -> Result<SeriesSearchResult, Box<dyn Error>> {
126 self.call("SeriesSearch", SeriesSearch(params)).await
127 }
128
129 pub async fn author_search(
130 &self,
131 params: AuthorSearchParams,
132 ) -> Result<AuthorSearchResult, Box<dyn Error>> {
133 self.call("AuthorSearch", AuthorSearch(params)).await
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use std::env;
140
141 use crate::actress_search::SortValue;
142 use crate::item_list::SiteValue;
143
144 use super::*;
145
146 #[tokio::test]
147 async fn test_item_list() {
148 let dmm = client();
149 let r = dmm
150 .item_list(ItemListParams {
151 site: SiteValue::Dmm,
152 hits: Some(100),
153 ..ItemListParams::default()
154 })
155 .await
156 .unwrap();
157
158 assert_eq!(r.status, 200);
162 }
163
164 #[tokio::test]
165 async fn test_floor_list() {
166 let dmm = client();
167 let r = dmm.floor_list(FloorListParams {}).await.unwrap();
168 assert_eq!(
169 r.site
170 .get(0)
171 .unwrap()
172 .service
173 .get(0)
174 .unwrap()
175 .floor
176 .get(0)
177 .unwrap()
178 .name,
179 "AKB48"
180 );
181 }
182 #[tokio::test]
183 async fn test_actress_search() {
184 let dmm = client();
185 let resp = dmm
186 .actress_search(ActressSearchParams {
187 keyword: Some("あさみ".to_string()),
188 gte_bust: Some(90),
189 lte_waist: Some(60),
190 sort: Some(SortValue::BustDesc),
191 offset: Some(1), ..ActressSearchParams::default()
193 })
194 .await
195 .unwrap();
196 assert_eq!(resp.first_position, 1);
197 let a = resp.actress.unwrap();
198 let g = a.iter().find(|g| g.id == "15365");
200 assert_eq!(g.unwrap().name, "麻美ゆま");
201
202 let resp = dmm
203 .actress_search(ActressSearchParams {
204 gte_birthday: Some(chrono::NaiveDate::from_ymd_opt(2000, 1, 1).unwrap()),
205 ..ActressSearchParams::default()
207 })
208 .await
209 .unwrap();
210 assert_eq!(resp.first_position, 1);
211 }
212
213 #[tokio::test]
214 async fn test_genre_search() {
215 let dmm = client();
216 let resp = dmm
217 .genre_search(GenreSearchParams {
218 floor_id: "25".to_string(),
219 initial: Some('き'),
220 hits: Some(10),
221 offset: Some(10),
222 })
223 .await
224 .unwrap();
225 let g = resp.genre.iter().find(|g| g.genre_id == "73115").unwrap();
226 assert_eq!(g.name, "キャラクター");
227 }
228
229 #[tokio::test]
230 async fn test_maker_search() {
231 let dmm = client();
232 let res = dmm
233 .series_search(SeriesSearchParams {
234 floor_id: "27".to_string(),
235 initial: Some('お'),
236 hits: Some(10),
237 ..SeriesSearchParams::default()
238 })
239 .await
240 .unwrap();
241 let s = res.series.iter().find(|m| m.series_id == "100864").unwrap();
242 assert_eq!(s.name, "おいしい銀座");
243 }
244
245 #[tokio::test]
246 async fn test_series_search() {
247 let dmm = client();
248 let res = dmm
249 .maker_search(MakerSearchParams {
250 floor_id: "27".to_string(),
251 initial: Some('こ'),
252 offset: Some(1),
253 ..MakerSearchParams::default()
254 })
255 .await
256 .unwrap();
257 let m = res.maker.iter().find(|m| m.maker_id == "93146").unwrap();
258 assert_eq!(m.name, "講談社");
259 }
260
261 #[tokio::test]
262 async fn test_author_search() {
263 let dmm = client();
264 let res = dmm
265 .author_search(AuthorSearchParams {
266 floor_id: "27".to_string(),
267 initial: Some('と'),
268 offset: Some(100),
269 ..AuthorSearchParams::default()
270 })
271 .await
272 .unwrap();
273 assert_eq!(res.status, "200");
274 }
275
276 fn client() -> Dmm {
277 let api_id = env::var("DMM_API_ID").unwrap();
278 let affiliate_id = env::var("DMM_AFFILIATE_ID").unwrap();
279 Dmm::new(&api_id, &affiliate_id)
280 }
281}