use strum::{Display, EnumCount, EnumIs, EnumIter};
use crate::convert::Parameterize;
use std::{
fmt::{Display, Formatter},
ops::RangeInclusive,
};
use thiserror::Error;
#[cfg_attr(
feature = "serde-support",
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(feature = "serde-support", serde(rename_all = "kebab-case"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, EnumIter, EnumIs, EnumCount)]
pub enum Category {
NonR18,
R18,
Mixin,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
feature = "serde-support",
derive(serde::Serialize, serde::Deserialize)
)]
pub(crate) struct Keyword(pub(crate) String);
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
feature = "serde-support",
derive(serde::Serialize, serde::Deserialize)
)]
pub(crate) struct Size(pub(crate) Vec<ImageSize>);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, EnumIter, EnumIs, EnumCount)]
#[cfg_attr(
feature = "serde-support",
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(feature = "serde-support", serde(rename_all = "kebab-case"))]
#[strum(serialize_all = "lowercase")]
pub enum ImageSize {
Original,
Regular,
Small,
Thumb,
Mini,
}
#[cfg_attr(
feature = "serde-support",
derive(serde::Serialize, serde::Deserialize)
)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Proxy(pub(crate) String);
#[cfg_attr(
feature = "serde-support",
derive(serde::Serialize, serde::Deserialize)
)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct DateAfter(pub(crate) u64);
#[cfg_attr(
feature = "serde-support",
derive(serde::Serialize, serde::Deserialize)
)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct DateBefore(pub(crate) u64);
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum Error {
#[error("excepted {range:?}, found {actual} {filed}")]
OutOfRange {
range: RangeInclusive<usize>,
actual: usize,
filed: &'static str,
},
#[error("each tag condition could only contain at most 20 OR tags!")]
InvalidTag,
#[cfg(feature = "aspect-validate")]
#[error("aspect ratio must match regex")]
InvalidAspectRatio,
}
#[must_use]
#[cfg_attr(
feature = "serde-support",
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(feature = "serde-support", serde(rename_all = "kebab-case"))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Request {
category: Category,
num: Num,
uid: Uid,
keyword: Option<Keyword>,
tag: Tag,
size: Size,
proxy: Proxy,
date_after: Option<DateAfter>,
date_before: Option<DateBefore>,
dsc: Dsc,
exclude_ai: ExcludeAI,
aspect_ratio: AspectRatio,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
feature = "serde-support",
derive(serde::Serialize, serde::Deserialize)
)]
pub(crate) struct Uid(pub Vec<u32>);
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
feature = "serde-support",
derive(serde::Serialize, serde::Deserialize)
)]
pub(crate) struct Tag(pub Vec<String>);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(
feature = "serde-support",
derive(serde::Serialize, serde::Deserialize)
)]
pub(crate) struct Num(pub u8);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(
feature = "serde-support",
derive(serde::Serialize, serde::Deserialize)
)]
pub(crate) struct Dsc(pub bool);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(
feature = "serde-support",
derive(serde::Serialize, serde::Deserialize)
)]
pub(crate) struct ExcludeAI(pub bool);
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
feature = "serde-support",
derive(serde::Serialize, serde::Deserialize)
)]
pub(crate) struct AspectRatio(pub Option<String>);
impl std::default::Default for Request {
fn default() -> Self {
Request {
category: Category::NonR18,
num: Num(1),
uid: Uid(vec![]),
keyword: None,
tag: Tag(vec![]),
size: Size(vec![ImageSize::Original]),
proxy: Proxy("i.pixiv.re".into()),
date_after: None,
date_before: None,
dsc: Dsc(false),
exclude_ai: ExcludeAI(false),
aspect_ratio: AspectRatio(None),
}
}
}
impl Request {
pub fn category(self, category: Category) -> Self {
Self { category, ..self }
}
pub fn num(self, amount: u8) -> Result<Self, Error> {
let valid_range = 1..=20;
if valid_range.contains(&(amount as usize)) {
Ok(Self {
num: Num(amount),
..self
})
} else {
Err(Error::OutOfRange {
range: valid_range,
actual: amount as usize,
filed: "",
})
}
}
pub fn uid(self, authors: &[u32]) -> Result<Self, Error> {
if (0..=20).contains(&authors.len()) {
Ok(Self {
uid: Uid(authors.into()),
..self
})
} else {
Err(Error::OutOfRange {
range: 0..=20,
actual: authors.len(),
filed: "uid",
})
}
}
pub fn keyword(self, keyword: impl Into<String>) -> Self {
Self {
keyword: Some(Keyword(keyword.into())),
..self
}
}
pub fn tag(self, tag: &[impl AsRef<str>]) -> Result<Self, Error> {
if (0..=3).contains(&tag.len()) {
if tag
.iter()
.map(AsRef::as_ref)
.any(|s| s.split("|").count() > 20)
{
Err(Error::InvalidTag)?
}
Ok(Self {
tag: Tag(tag.iter().map(AsRef::as_ref).map(String::from).collect()),
..self
})
} else {
Err(Error::OutOfRange {
range: 0..=3,
actual: tag.len(),
filed: "AND tag",
})
}
}
pub fn size(self, size_list: &[ImageSize]) -> Result<Self, Error> {
match size_list.len() {
0..=5 => Ok(Self {
size: Size(size_list.into()),
..self
}),
_ => Err(Error::OutOfRange {
range: 0..=5,
actual: size_list.len(),
filed: "size",
}),
}
}
pub fn proxy(self, proxy: impl Into<String>) -> Self {
Self {
proxy: Proxy(proxy.into()),
..self
}
}
pub fn date_after(self, date_after: u64) -> Self {
Self {
date_after: Some(DateAfter(date_after)),
..self
}
}
pub fn date_before(self, date_before: u64) -> Self {
Self {
date_before: Some(DateBefore(date_before)),
..self
}
}
pub fn dsc(self, dsc: bool) -> Self {
Self {
dsc: Dsc(dsc),
..self
}
}
pub fn exclude_ai(self, exclude_ai: bool) -> Self {
Self {
exclude_ai: ExcludeAI(exclude_ai),
..self
}
}
pub fn aspect_ratio(self, aspect_ratio: impl AsRef<str>) -> Result<Self, Error> {
let aspect_ratio = aspect_ratio.as_ref();
#[cfg(feature = "aspect-validate")]
{
use regex::Regex;
use std::sync::LazyLock;
static RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r#"^((gt|gte|lt|lte|eq)[\d.]+){1,2}$"#).unwrap());
if !RE.is_match(aspect_ratio) {
Err(Error::InvalidAspectRatio)?
}
}
Ok(Self {
aspect_ratio: AspectRatio(Some(aspect_ratio.into())),
..self
})
}
}
impl Display for Request {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let mut url: String = "https://api.lolicon.app/setu/v2?".into();
url.append(&self.category);
url.append(&self.date_after);
url.append(&self.date_before);
url.append(&self.dsc);
url.append(&self.keyword);
url.append(&self.num);
url.append(&self.proxy);
url.append(&self.size);
url.append(&self.tag);
url.append(&self.uid);
url.append(&self.exclude_ai);
url.append(&self.aspect_ratio);
write!(f, "{}", url)
}
}
impl From<Request> for String {
fn from(request: Request) -> Self {
request.to_string()
}
}
trait AddArgument {
fn append(&mut self, option: &impl Parameterize);
}
impl AddArgument for String {
fn append(&mut self, option: &impl Parameterize) {
option.param(self);
}
}