use fang::FangError;
use lazy_static::lazy_static;
use log::debug;
use log::error;
use rand::Rng;
use reqwest::Client;
use reqwest::Proxy;
use reqwest::Response;
use reqwest::StatusCode;
use reqwest_cookie_store::CookieStore;
use reqwest_cookie_store::CookieStoreMutex;
use std::fmt;
use std::sync::Arc;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
use thiserror::Error;
use crate::model::filter::Currency;
use crate::model::filter::Filter;
use crate::model::item::AdvancedItem;
use crate::model::items::AdvancedItems;
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, String)),
}
#[derive(Error, Debug)]
pub enum VintedWrapperError {
#[error(transparent)]
ReqWestError(#[from] reqwest::Error),
#[error(transparent)]
SerdeError(#[from] SerdeJSONError),
#[error(transparent)]
CookiesError(#[from] CookieError),
#[error("Number of items must be non-zero value")]
ItemNumberError,
#[error("Could not get deatiled info for item `{2}` with code: {0}")]
ItemError(StatusCode, Option<i32>, String),
}
#[derive(Debug, Error)]
pub struct SerdeJSONError {
raw_json: String,
serde_error: serde_json::Error,
}
impl SerdeJSONError {
fn new(raw_json: String, serde_error: serde_json::Error) -> Self {
SerdeJSONError {
raw_json,
serde_error,
}
}
}
impl fmt::Display for SerdeJSONError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"JSON: {}, SerdeError: {}",
self.raw_json, self.serde_error
)
}
}
impl From<VintedWrapperError> for FangError {
fn from(value: VintedWrapperError) -> FangError {
FangError {
description: format!("{value:?}"),
}
}
}
const DOMAINS: [&str; 22] = [
"fr", "es", "lu", "nl", "lt", "de", "at", "it", "co.uk", "pt", "com", "cz", "sk", "pl", "se",
"ro", "hu", "fi", "gr", "ie", "hr", "dk",
];
const DEFAULT_USER_AGENT: &str = "*/*";
#[derive(Debug, Clone)]
pub enum Host {
Fr,
Es,
Lu,
Nl,
Lt,
De,
At,
It,
Uk,
Pt,
Com,
Cz,
Sk,
Pl,
Se,
Ro,
Hu,
Fi,
Gr,
Ie,
Hr,
Dk,
}
impl Host {
pub fn is_euro_host(&self) -> bool {
matches!(
self,
Host::Es
| Host::It
| Host::Fr
| Host::Pt
| Host::Fi
| Host::Gr
| Host::Ie
| Host::Lu
| Host::Nl
| Host::At
| Host::De
| Host::Lt )
}
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::rng().random_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,
"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,
"fi" => Host::Fi,
"gr" => Host::Gr,
"ie" => Host::Ie,
"hr" => Host::Hr,
"dk" => Host::Dk,
_ => panic!("Not valid host"),
}
}
}
impl From<Host> for &str {
fn from(val: Host) -> Self {
match val {
Host::Fr => DOMAINS[0],
Host::Es => DOMAINS[1],
Host::Lu => DOMAINS[2],
Host::Nl => DOMAINS[3],
Host::Lt => DOMAINS[4],
Host::De => DOMAINS[5],
Host::At => DOMAINS[6],
Host::It => DOMAINS[7],
Host::Uk => DOMAINS[8],
Host::Pt => DOMAINS[9],
Host::Com => DOMAINS[10],
Host::Cz => DOMAINS[11],
Host::Sk => DOMAINS[12],
Host::Pl => DOMAINS[13],
Host::Se => DOMAINS[14],
Host::Ro => DOMAINS[15],
Host::Hu => DOMAINS[16],
Host::Fi => DOMAINS[17],
Host::Gr => DOMAINS[18],
Host::Ie => DOMAINS[19],
Host::Hr => DOMAINS[20],
Host::Dk => DOMAINS[21],
}
}
}
pub fn random_host<'a>() -> &'a str {
let random_index = rand::rng().random_range(0..DOMAINS.len());
DOMAINS[random_index]
}
lazy_static! {
pub static ref COOKIE_STORE: Arc<CookieStoreMutex> = {
let cookie_store = CookieStore::new(None);
let cookie_store = CookieStoreMutex::new(cookie_store);
Arc::new(cookie_store)
};
}
async fn get_client(user_agent: Option<&str>, proxy: Option<Proxy>) -> Client {
if let Some(proxy) = proxy {
create_client_proxy(user_agent, proxy)
} else {
create_client(user_agent)
}
}
fn create_client_proxy(user_agent: Option<&str>, proxy: Proxy) -> Client {
reqwest::ClientBuilder::new()
.user_agent(user_agent.unwrap_or(DEFAULT_USER_AGENT))
.proxy(proxy)
.cookie_provider(COOKIE_STORE.clone())
.build()
.unwrap()
}
fn create_client(user_agent: Option<&str>) -> Client {
reqwest::ClientBuilder::new()
.user_agent(user_agent.unwrap_or(DEFAULT_USER_AGENT))
.cookie_provider(COOKIE_STORE.clone())
.build()
.unwrap()
}
#[derive(Debug, Clone)]
pub struct VintedWrappers<'a> {
wrappers: Vec<VintedWrapper<'a>>,
pub len: usize,
}
impl VintedWrappers<'_> {
pub fn new_with_hosts(hosts: Vec<Host>) -> Self {
let len = hosts.len();
let wrappers = hosts
.into_iter()
.map(VintedWrapper::new_with_host)
.collect();
VintedWrappers { wrappers, len }
}
pub fn all_wrappers() -> Self {
let hosts = vec![
Host::Es,
Host::Fr,
Host::Lu,
Host::Pt,
Host::It,
Host::Nl,
Host::Lt,
Host::De,
Host::At,
Host::Uk,
Host::Com,
Host::Cz,
Host::Sk,
Host::Pl,
Host::Se,
Host::Ro,
Host::Hu,
Host::Fi,
Host::Gr,
Host::Ie,
Host::Hr,
Host::Dk,
];
VintedWrappers::new_with_hosts(hosts)
}
pub fn get_wrapper(&self, index: usize) -> &VintedWrapper<'_> {
&self.wrappers[index]
}
pub async fn lineal_fetch(
&mut self,
filters: &Filter,
num: u32,
current: usize,
user_agent: Option<&str>,
proxy_cookies: Option<Proxy>,
proxy_fetch: Option<Proxy>,
) -> Result<Items, VintedWrapperError> {
let vinted_wrapper = &self.wrappers[current];
vinted_wrapper
.get_items(filters, num, user_agent, proxy_cookies, proxy_fetch)
.await
}
pub async fn lineal_to_advance_items(
&mut self,
item_id: i64,
current: usize,
user_agent: Option<&str>,
proxy_cookies: Option<Proxy>,
proxy_fetch: Option<Proxy>,
) -> Result<AdvancedItem, VintedWrapperError> {
let vinted_wrapper = &self.wrappers[current];
vinted_wrapper
.get_advanced_item(item_id, user_agent, proxy_cookies, proxy_fetch)
.await
}
}
impl Default for VintedWrappers<'_> {
fn default() -> Self {
let hosts = vec![Host::Es, Host::Fr, Host::Lu, Host::Pt, Host::It, Host::Nl];
VintedWrappers::new_with_hosts(hosts)
}
}
#[derive(Debug, Clone)]
pub struct VintedWrapper<'a> {
id: usize,
host: &'a str,
}
static WRAPPER_ID: AtomicUsize = AtomicUsize::new(0);
impl Default for VintedWrapper<'_> {
fn default() -> Self {
Self::new_with_host(Host::Es)
}
}
impl VintedWrapper<'_> {
pub fn new() -> Self {
let id = WRAPPER_ID.fetch_add(1, Ordering::SeqCst);
VintedWrapper {
host: random_host(),
id,
}
}
pub fn new_with_host(host: Host) -> Self {
let id = WRAPPER_ID.fetch_add(1, Ordering::SeqCst);
VintedWrapper {
host: host.into(),
id,
}
}
pub fn new_with_currency(currency: Currency) -> Self {
VintedWrapper::new_with_host(currency.into())
}
pub fn get_host(&self) -> &str {
self.host
}
pub fn get_id(&self) -> &usize {
&self.id
}
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;
}
}
pub async fn get_cookies(
&self,
user_agent: Option<&str>,
proxy: Option<Proxy>,
) -> Result<(), CookieError> {
let client = get_client(user_agent, proxy).await;
let request = format!("https://www.vinted.{}/", self.host);
let mut response_cookies = client.get(&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()),
user_agent.unwrap_or(DEFAULT_USER_AGENT).to_string(),
)));
}
Ok(())
}
pub async fn refresh_cookies(
&self,
user_agent: Option<&str>,
proxy: Option<Proxy>,
) -> Result<(), CookieError> {
let client = get_client(user_agent, proxy).await;
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()),
user_agent.unwrap_or(DEFAULT_USER_AGENT).to_string(),
)));
}
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,
user_agent: Option<&str>,
proxy_cookies: Option<Proxy>,
proxy_fetch: Option<Proxy>,
) -> Result<Items, VintedWrapperError> {
if num == 0 {
return Err(VintedWrapperError::ItemNumberError);
}
let client = get_client(user_agent, proxy_fetch).await;
let domain: &str = &format!("vinted.{}", self.host);
let cookie_not_valid;
{
let cookie_store_clone = COOKIE_STORE.lock().unwrap();
cookie_not_valid = cookie_store_clone.get(domain, "/", "__cf_bm").is_none();
}
if cookie_not_valid {
debug!(
"[{}] POST_GET_COOKIES -> Get {} items @ {}",
self.id, num, self.host
);
self.get_cookies(user_agent, proxy_cookies).await?;
}
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}");
debug!("[{}] GET_{}_ITEMS @ {}", self.id, num, self.host);
let json: Response = client.get(url).send().await?;
match json.status() {
StatusCode::OK => {
let raw_json = json.text().await?;
match serde_json::from_str::<Items>(&raw_json) {
Ok(items) => Ok(items),
Err(serde_error) => {
let error = SerdeJSONError::new(raw_json, serde_error);
error!("Failed to deserialize: {}", error); Err(VintedWrapperError::SerdeError(error))
}
}
}
code => {
let retry_after = json
.headers()
.get("retry-after")
.map(|value| value.to_str().unwrap().to_string().parse().unwrap());
Err(VintedWrapperError::ItemError(
code,
retry_after,
format!("{}::{}", self.host, json.url()),
))
}
}
}
pub async fn get_advanced_item(
&self,
item_id: i64,
user_agent: Option<&str>,
proxy_cookies: Option<Proxy>,
proxy_fetch: Option<Proxy>,
) -> Result<AdvancedItem, VintedWrapperError> {
let client = get_client(user_agent, proxy_fetch).await;
let domain: &str = &format!("vinted.{}", self.host);
let cookie_not_valid;
{
let cookie_store_clone = COOKIE_STORE.lock().unwrap();
cookie_not_valid = cookie_store_clone.get(domain, "/", "__cf_bm").is_none();
}
if cookie_not_valid {
debug!(
"[{}] POST_GET_COOKIES -> Get item {} @ {}",
self.id, item_id, self.host
);
self.get_cookies(user_agent, proxy_cookies).await?;
}
let url = format!("https://www.vinted.{}/api/v2/items/{}", self.host, item_id);
debug!(
"[{}] GET_ADVANCED_ITEM_{} @ {}",
self.id, item_id, self.host
);
let json: Response = client.get(url).send().await?;
match json.status() {
StatusCode::OK => {
let raw_json = json.text().await?;
match serde_json::from_str::<AdvancedItems>(&raw_json) {
Ok(items) => Ok(items.item),
Err(serde_error) => {
let error = SerdeJSONError::new(raw_json, serde_error);
error!("Failed to deserialize: {}", error); Err(VintedWrapperError::SerdeError(error))
}
}
}
code => {
let retry_after = json
.headers()
.get("retry-after")
.map(|value| value.to_str().unwrap().to_string().parse().unwrap());
Err(VintedWrapperError::ItemError(
code,
retry_after,
format!("{}::{}", self.host, item_id),
))
}
}
}
}