use once_cell::sync::OnceCell;
use rand::Rng;
use reqwest::Client;
use reqwest::StatusCode;
use reqwest_cookie_store::CookieStore;
use reqwest_cookie_store::CookieStoreMutex;
use std::sync::Arc;
use thiserror::Error;
use crate::model::filter::Currency;
use crate::model::filter::Filter;
use crate::model::items::Items;
#[derive(Error, Debug)]
pub enum CookieError {
#[error(transparent)]
ReqWestError(#[from] reqwest::Error),
#[error("Error to get cookies")]
GetCookiesError((StatusCode, String)),
}
#[derive(Error, Debug)]
pub enum VintedWrapperError {
#[error(transparent)]
CookiesError(#[from] CookieError),
#[error("Number of items must be non-zero value")]
ItemNumberError,
}
impl From<reqwest::Error> for VintedWrapperError {
fn from(value: reqwest::Error) -> Self {
VintedWrapperError::CookiesError(CookieError::ReqWestError(value))
}
}
const DOMAINS: [&str; 18] = [
"fr", "be", "es", "lu", "nl", "lt", "de", "at", "it", "co.uk", "pt", "com", "cz", "sk", "pl",
"se", "ro", "hu",
];
#[derive(Debug, Clone)]
pub enum Host {
Fr,
Be,
Es,
Lu,
Nl,
Lt,
De,
At,
It,
Uk,
Pt,
Com,
Cz,
Sk,
Pl,
Se,
Ro,
Hu,
}
impl Host {
pub fn is_euro_host(&self) -> bool {
!matches!(
self,
Host::Com | Host::Uk | Host::Cz | Host::Pl | Host::Se | Host::Ro | Host::Hu
)
}
pub fn random_euro_host() -> Self {
let domains_euro: Vec<Host> = DOMAINS
.iter()
.map(|domain_str| (*domain_str).into())
.filter(|domain: &Host| domain.is_euro_host())
.collect();
let random_index = rand::thread_rng().gen_range(0..domains_euro.len());
domains_euro[random_index].clone()
}
}
impl From<&str> for Host {
fn from(string: &str) -> Self {
match string {
"fr" => Host::Fr,
"be" => Host::Be,
"es" => Host::Es,
"lu" => Host::Lu,
"nl" => Host::Nl,
"lt" => Host::Lt,
"de" => Host::De,
"at" => Host::At,
"it" => Host::It,
"co.uk" => Host::Uk,
"pt" => Host::Pt,
"com" => Host::Com,
"cz" => Host::Cz,
"sk" => Host::Sk,
"pl" => Host::Pl,
"se" => Host::Se,
"ro" => Host::Ro,
"hu" => Host::Hu,
_ => panic!("Not valid host"),
}
}
}
impl From<Host> for &str {
fn from(val: Host) -> Self {
match val {
Host::Fr => DOMAINS[0],
Host::Be => DOMAINS[1],
Host::Es => DOMAINS[2],
Host::Lu => DOMAINS[3],
Host::Nl => DOMAINS[4],
Host::Lt => DOMAINS[5],
Host::De => DOMAINS[6],
Host::At => DOMAINS[7],
Host::It => DOMAINS[8],
Host::Uk => DOMAINS[9],
Host::Pt => DOMAINS[10],
Host::Com => DOMAINS[11],
Host::Cz => DOMAINS[12],
Host::Sk => DOMAINS[13],
Host::Pl => DOMAINS[14],
Host::Se => DOMAINS[15],
Host::Ro => DOMAINS[16],
Host::Hu => DOMAINS[17],
}
}
}
pub fn random_host<'a>() -> &'a str {
let random_index = rand::thread_rng().gen_range(0..DOMAINS.len());
DOMAINS[random_index]
}
static CLIENT: OnceCell<Client> = OnceCell::new();
#[derive(Debug, Clone)]
pub struct VintedWrapper<'a> {
host: &'a str,
cookie_store: Arc<CookieStoreMutex>,
}
impl<'a> Default for VintedWrapper<'a> {
fn default() -> Self {
Self::new_with_host(Host::Es)
}
}
impl<'a> VintedWrapper<'a> {
pub fn new() -> Self {
let cookie_store = CookieStore::new(None);
let cookie_store = CookieStoreMutex::new(cookie_store);
let cookie_store = Arc::new(cookie_store);
VintedWrapper {
host: random_host(),
cookie_store,
}
}
pub fn new_with_host(host: Host) -> Self {
let cookie_store = CookieStore::new(None);
let cookie_store = CookieStoreMutex::new(cookie_store);
let cookie_store = Arc::new(cookie_store);
VintedWrapper {
host: host.into(),
cookie_store,
}
}
pub fn new_with_currency(currency: Currency) -> Self {
VintedWrapper::new_with_host(currency.into())
}
pub fn get_host(&self) -> &str {
self.host
}
pub fn set_new_random_host(&mut self) {
self.host = random_host();
}
pub fn set_new_host(&mut self, host: Host) {
self.host = host.into();
}
pub fn set_host_by_currency(&mut self, currency: Currency) {
let host: Host = currency.into();
let actual_host: Host = self.host.into();
if !host.is_euro_host() || !actual_host.is_euro_host() {
let host_str: &str = host.into();
self.host = host_str;
}
}
fn get_client(&self) -> &'static Client {
CLIENT.get_or_init(|| -> Client {
reqwest::ClientBuilder::new()
.user_agent("PostmanRuntime/7.32.3")
.cookie_provider(Arc::clone(&self.cookie_store))
.build()
.unwrap()
})
}
pub async fn refresh_cookies(&self) -> Result<(), CookieError> {
self.cookie_store.lock().unwrap().clear();
let client = self.get_client();
let request = format!("https://www.vinted.{}/auth/token_refresh", self.host);
let mut response_cookies = client.post(&request).send().await?;
let max_retries = 3;
let mut i = 0;
while response_cookies.status() != StatusCode::OK && i < max_retries {
response_cookies = client.post(&request).send().await?;
i += 1;
}
if response_cookies.status() != StatusCode::OK {
return Err(CookieError::GetCookiesError((
response_cookies.status(),
String::from(self.get_host()),
)));
}
Ok(())
}
fn substitute_if_first(first: &mut bool, url: &mut String) {
if *first {
url.replace_range(0..1, "?");
*first = false;
}
}
pub async fn get_items(&self, filters: &Filter, num: u32) -> Result<Items, VintedWrapperError> {
if num == 0 {
return Err(VintedWrapperError::ItemNumberError);
}
let domain: &str = &format!("https://www.vinted.{}/api/v2/catalog/items", self.host);
let cookie_store_clone = self.cookie_store.clone();
if cookie_store_clone
.lock()
.unwrap()
.get(domain, "/", "__cf_bm")
.is_none()
{
self.refresh_cookies().await?;
}
let client = self.get_client();
let mut first = true;
let mut url = format!("https://www.vinted.{}/api/v2/catalog/items", self.host);
if let Some(text) = &filters.search_text {
url = format!("{url}?search_text={text}");
first = false;
}
if let Some(catalog_ids) = &filters.catalog_ids {
let mut catalog_args: String = format!("&catalog_ids={}", catalog_ids);
VintedWrapper::substitute_if_first(&mut first, &mut catalog_args);
url = format!("{url}{catalog_args}");
}
if let Some(color_ids) = &filters.color_ids {
let mut color_args: String = format!("&color_ids={}", color_ids);
VintedWrapper::substitute_if_first(&mut first, &mut color_args);
url = format!("{url}{color_args}");
}
if let Some(brand_ids) = &filters.brand_ids {
let mut brand_args: String = format!("&brand_ids={}", brand_ids);
VintedWrapper::substitute_if_first(&mut first, &mut brand_args);
url = format!("{url}{brand_args}");
}
if let Some(size_ids) = &filters.size_ids {
let mut size_args: String = format!("&size_ids={}", size_ids);
VintedWrapper::substitute_if_first(&mut first, &mut size_args);
url = format!("{url}{size_args}");
}
if let Some(material_ids) = &filters.material_ids {
let mut material_args: String = format!("&material_ids={}", material_ids);
VintedWrapper::substitute_if_first(&mut first, &mut material_args);
url = format!("{url}{material_args}");
}
if let Some(countries_ids) = &filters.countries_ids {
let mut countries_args: String = format!("&country_ids={}", countries_ids);
VintedWrapper::substitute_if_first(&mut first, &mut countries_args);
url = format!("{url}{countries_args}");
}
if let Some(price_from) = &filters.price_from {
let mut price_from_arg: String = format!("&price_from={}", price_from);
VintedWrapper::substitute_if_first(&mut first, &mut price_from_arg);
url = format!("{url}{price_from_arg}");
}
if let Some(price_to) = &filters.price_to {
let mut price_to_arg: String = format!("&price_to={}", price_to);
VintedWrapper::substitute_if_first(&mut first, &mut price_to_arg);
url = format!("{url}{price_to_arg}");
}
if let Some(vec) = &filters.article_status {
let querify_vec: Vec<&str> = vec.iter().map(|status| status.into()).collect();
let mut article_status_args: String = format!("&status_ids={}", querify_vec.join(","));
VintedWrapper::substitute_if_first(&mut first, &mut article_status_args);
url = format!("{url}{article_status_args}");
}
if let Some(sort_by) = &filters.sort_by {
let sort_by_str: &str = sort_by.into();
let mut sort_by_arg = format!("&order={}", sort_by_str);
VintedWrapper::substitute_if_first(&mut first, &mut sort_by_arg);
url = format!("{url}{sort_by_arg}");
}
let mut per_page_args = format!("&per_page={num}");
VintedWrapper::substitute_if_first(&mut first, &mut per_page_args);
url = format!("{url}{per_page_args}");
let items: Items = client.get(url).send().await?.json().await?;
Ok(items)
}
}