dmm_api/
dmm.rs

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(&params)?;
77        // println!("{}", &qs);
78        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        // println!("{}", &text);
83        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        // for i in &r.items.items {
159        //     println!("{:?}", i.date)
160        // }
161        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), // given that, returns string first_position
192                ..ActressSearchParams::default()
193            })
194            .await
195            .unwrap();
196        assert_eq!(resp.first_position, 1);
197        let a = resp.actress.unwrap();
198        // a.iter().for_each(|e| println!("{:?}", &e.birthday));
199        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                // not given offset, returns number first_position
206                ..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}