use std::error::Error;
use reqwest::Url;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use crate::actress_search::{ActressSearchParams, ActressSearchResult};
use crate::author_search::{AuthorSearchParams, AuthorSearchResult};
use crate::dmm::ApiParams::{
ActressSearch, AuthorSearch, FloorList, GenreSearch, ItemList, MakerSearch, SeriesSearch,
};
use crate::floor_list::{FloorListParams, FloorListResult};
use crate::genre_search::{GenreSearchParams, GenreSearchResult};
use crate::item_list::{ItemListParams, ItemListResult};
use crate::maker_search::{MakerSearchParams, MakerSearchResult};
use crate::series_search::{SeriesSearchParams, SeriesSearchResult};
const ENDPOINT_BASE: &str = "https://api.dmm.com/affiliate/v3/";
#[derive(Serialize, Debug)]
#[serde(untagged)]
#[allow(clippy::large_enum_variant)]
enum ApiParams {
ItemList(ItemListParams),
FloorList(FloorListParams),
ActressSearch(ActressSearchParams),
GenreSearch(GenreSearchParams),
MakerSearch(MakerSearchParams),
SeriesSearch(SeriesSearchParams),
AuthorSearch(AuthorSearchParams),
}
#[derive(Deserialize, Debug)]
struct ApiResponse<T: ApiResult> {
pub result: T,
}
pub(crate) trait ApiResult {}
#[derive(Deserialize, Debug)]
pub struct ElementVec<T> {
#[serde(rename = "item")]
pub items: Vec<T>,
}
pub struct Dmm {
api_id: String,
affiliate_id: String,
}
impl Dmm {
pub fn new(dmm_api_id: &str, dmm_affiliate_id: &str) -> Self {
Dmm {
api_id: dmm_api_id.to_string(),
affiliate_id: dmm_affiliate_id.to_string(),
}
}
async fn call<T>(&self, api: &str, params: ApiParams) -> Result<T, Box<dyn Error>>
where
T: ApiResult + DeserializeOwned,
{
let auth = querystring::stringify(vec![
("api_id", &self.api_id),
("affiliate_id", &self.affiliate_id),
("output", "xml"),
]);
let qs = serde_qs::to_string(¶ms)?;
let mut url = Url::parse(ENDPOINT_BASE)?.join(api)?;
url.set_query(Some(&(auth + &qs)));
let res = reqwest::get(url).await?;
let text = res.text().await?;
let res: ApiResponse<T> = serde_xml_rs::from_str(&text)?;
Ok(res.result)
}
pub async fn item_list(
&self,
params: ItemListParams,
) -> Result<ItemListResult, Box<dyn Error>> {
self.call("ItemList", ItemList(params)).await
}
pub async fn floor_list(
&self,
params: FloorListParams,
) -> Result<FloorListResult, Box<dyn Error>> {
self.call("FloorList", FloorList(params)).await
}
pub async fn actress_search(
&self,
params: ActressSearchParams,
) -> Result<ActressSearchResult, Box<dyn Error>> {
self.call("ActressSearch", ActressSearch(params)).await
}
pub async fn genre_search(
&self,
params: GenreSearchParams,
) -> Result<GenreSearchResult, Box<dyn Error>> {
self.call("GenreSearch", GenreSearch(params)).await
}
pub async fn maker_search(
&self,
params: MakerSearchParams,
) -> Result<MakerSearchResult, Box<dyn Error>> {
self.call("MakerSearch", MakerSearch(params)).await
}
pub async fn series_search(
&self,
params: SeriesSearchParams,
) -> Result<SeriesSearchResult, Box<dyn Error>> {
self.call("SeriesSearch", SeriesSearch(params)).await
}
pub async fn author_search(
&self,
params: AuthorSearchParams,
) -> Result<AuthorSearchResult, Box<dyn Error>> {
self.call("AuthorSearch", AuthorSearch(params)).await
}
}
#[cfg(test)]
mod tests {
use std::env;
use crate::actress_search::SortValue;
use crate::item_list::SiteValue;
use super::*;
#[tokio::test]
async fn test_item_list() {
let dmm = client();
let r = dmm
.item_list(ItemListParams {
site: SiteValue::Dmm,
hits: Some(100),
..ItemListParams::default()
})
.await
.unwrap();
assert_eq!(r.status, 200);
}
#[tokio::test]
async fn test_floor_list() {
let dmm = client();
let r = dmm.floor_list(FloorListParams {}).await.unwrap();
assert_eq!(
r.site
.items
.get(0)
.unwrap()
.service
.items
.get(0)
.unwrap()
.floor
.items
.get(0)
.unwrap()
.name,
"AKB48"
);
}
#[tokio::test]
async fn test_actress_search() {
let dmm = client();
let resp = dmm
.actress_search(ActressSearchParams {
keyword: Some("あさみ".to_string()),
gte_bust: Some(90),
lte_waist: Some(60),
sort: Some(SortValue::BustDesc),
offset: Some(1), ..ActressSearchParams::default()
})
.await
.unwrap();
assert_eq!(resp.first_position, 1);
let a = resp.actress.unwrap().items;
let g = a.iter().find(|g| g.id == "15365");
assert_eq!(g.unwrap().name, "麻美ゆま");
let resp = dmm
.actress_search(ActressSearchParams {
gte_birthday: Some(chrono::NaiveDate::from_ymd_opt(2000, 1, 1).unwrap()),
..ActressSearchParams::default()
})
.await
.unwrap();
assert_eq!(resp.first_position, 1);
}
#[tokio::test]
async fn test_genre_search() {
let dmm = client();
let resp = dmm
.genre_search(GenreSearchParams {
floor_id: "25".to_string(),
initial: Some('き'),
hits: Some(10),
offset: Some(10),
})
.await
.unwrap();
let g = resp
.genre
.items
.iter()
.find(|g| g.genre_id == "73115")
.unwrap();
assert_eq!(g.name, "キャラクター");
}
#[tokio::test]
async fn test_maker_search() {
let dmm = client();
let res = dmm
.series_search(SeriesSearchParams {
floor_id: "27".to_string(),
initial: Some('お'),
hits: Some(10),
..SeriesSearchParams::default()
})
.await
.unwrap();
let s = res
.series
.items
.iter()
.find(|m| m.series_id == "100864")
.unwrap();
assert_eq!(s.name, "おいしい銀座");
}
#[tokio::test]
async fn test_series_search() {
let dmm = client();
let res = dmm
.maker_search(MakerSearchParams {
floor_id: "27".to_string(),
initial: Some('こ'),
offset: Some(1),
..MakerSearchParams::default()
})
.await
.unwrap();
let m = res
.maker
.items
.iter()
.find(|m| m.maker_id == "93146")
.unwrap();
assert_eq!(m.name, "講談社");
}
#[tokio::test]
async fn test_author_search() {
let dmm = client();
let res = dmm
.author_search(AuthorSearchParams {
floor_id: "27".to_string(),
initial: Some('と'),
offset: Some(100),
..AuthorSearchParams::default()
})
.await
.unwrap();
assert_eq!(res.status, "200");
}
fn client() -> Dmm {
let api_id = env::var("DMM_API_ID").unwrap();
let affiliate_id = env::var("DMM_AFFILIATE_ID").unwrap();
Dmm::new(&api_id, &affiliate_id)
}
}