Files
wallabicher/platforms/vinted_platform.py
Omar Sánchez Pizarro 4111f57564 add abstraction ob platform and article + vinted
"

Signed-off-by: Omar Sánchez Pizarro <omar.sanchez@pistacero.net>
2025-10-10 14:58:27 +02:00

308 lines
12 KiB
Python

"""
Vinted Platform Implementation
Uses Vinted's internal API for product search
"""
import requests
import logging
import time
from datetime import datetime
from platforms.base_platform import BasePlatform
from models.article import Article
REQUEST_RETRY_TIME = 5
class VintedPlatform(BasePlatform):
"""Vinted marketplace platform implementation"""
# Mapping de dominios por país
COUNTRY_DOMAINS = {
'es': 'vinted.es',
'fr': 'vinted.fr',
'de': 'vinted.de',
'it': 'vinted.it',
'pl': 'vinted.pl',
'cz': 'vinted.cz',
'lt': 'vinted.lt',
'uk': 'vinted.co.uk',
'us': 'vinted.com',
'nl': 'vinted.nl',
'be': 'vinted.be',
'at': 'vinted.at',
}
def __init__(self, item_monitor):
super().__init__(item_monitor)
self.logger = logging.getLogger(__name__)
# Por defecto España, se puede configurar con un campo 'country' en item_monitor
self.country = getattr(item_monitor, '_country', 'es')
self.domain = self.COUNTRY_DOMAINS.get(self.country, 'vinted.es')
self.session = requests.Session()
self._init_session()
def _init_session(self):
"""Initialize session with proper cookies and headers"""
try:
# Primera petición para obtener cookies
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'es-ES,es;q=0.9,en;q=0.8',
'Accept-Encoding': 'gzip, deflate, br',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1',
}
response = self.session.get(f'https://www.{self.domain}', headers=headers, timeout=15)
self.logger.info(f"Vinted session initialized for {self.domain}")
except Exception as e:
self.logger.warning(f"Could not initialize Vinted session: {e}")
def get_platform_name(self):
return "vinted"
def create_url(self):
"""Construir URL de búsqueda de Vinted"""
# API interna de Vinted
base_url = f"https://www.{self.domain}/api/v2/catalog/items"
params = []
# Query de búsqueda
search_query = self._item_monitor.get_search_query()
if search_query:
params.append(f"search_text={requests.utils.quote(search_query)}")
# Ordenar por más reciente
params.append("order=newest_first")
# Precio (Vinted usa céntimos, multiplicamos por 100)
if self._item_monitor.get_min_price() != 0:
price_cents = int(self._item_monitor.get_min_price() * 100)
params.append(f"price_from={price_cents}")
if self._item_monitor.get_max_price() != 0:
price_cents = int(self._item_monitor.get_max_price() * 100)
params.append(f"price_to={price_cents}")
# Resultados por página (máximo suele ser 96)
params.append("per_page=96")
# Página (por defecto la primera)
params.append("page=1")
# Mapeo de condiciones Wallapop -> Vinted
condition = self._item_monitor.get_condition()
if condition != "all":
vinted_status = self._map_condition_to_vinted(condition)
if vinted_status:
params.append(f"status_ids[]={vinted_status}")
url = base_url
if params:
url += "?" + "&".join(params)
return url
def _map_condition_to_vinted(self, wallapop_condition):
"""
Mapear condiciones de Wallapop a IDs de estado de Vinted
Vinted status IDs: 1=Satisfactory, 2=Good, 3=Very Good, 6=Brand new with tag, 7=Brand new without tag
"""
mapping = {
'new': '6', # Brand new with tag
'as_good_as_new': '7', # Brand new without tag
'good': '3', # Very Good
'fair': '2', # Good
'has_given_it_all': '1' # Satisfactory
}
return mapping.get(wallapop_condition)
def get_request_headers(self):
"""Headers específicos para Vinted"""
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'es-ES,es;q=0.9,en;q=0.8',
'Accept-Encoding': 'gzip, deflate, br',
'Referer': f'https://www.{self.domain}/',
'Origin': f'https://www.{self.domain}',
'Connection': 'keep-alive',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
'Sec-Ch-Ua': '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
'Sec-Ch-Ua-Mobile': '?0',
'Sec-Ch-Ua-Platform': '"Windows"',
}
return headers
def fetch_articles(self):
"""Obtener artículos desde Vinted"""
url = self.create_url()
max_retries = 3
for attempt in range(max_retries):
try:
headers = self.get_request_headers()
response = self.session.get(url, headers=headers, timeout=30, allow_redirects=True)
response.raise_for_status()
json_response = response.json()
# Verificar estructura de respuesta
if 'items' not in json_response:
self.logger.warning(f"Unexpected Vinted response structure. Keys: {list(json_response.keys())}")
# Intentar ver si hay un mensaje de error
if 'error' in json_response:
self.logger.error(f"Vinted API error: {json_response['error']}")
return []
# INSERT_YOUR_CODE
json_items = json_response['items']
articles = self.parse_response(json_items)
return articles
except requests.exceptions.HTTPError as err:
status_code = err.response.status_code
self.logger.error(f"Vinted HTTP Error {status_code}: {err}")
if status_code == 401 or status_code == 403:
self.logger.warning("Vinted authentication issue, reinitializing session...")
self._init_session()
elif status_code == 429:
self.logger.warning("Vinted rate limit hit, waiting longer...")
time.sleep(REQUEST_RETRY_TIME * 3)
elif status_code == 404:
self.logger.error("Vinted API endpoint not found. Check URL.")
return []
# Log response content for debugging
try:
self.logger.debug(f"Response content: {err.response.text[:500]}")
except:
pass
except requests.exceptions.RequestException as err:
self.logger.error(f"Vinted Request Exception: {err}")
except ValueError as e:
self.logger.error(f"Error parsing JSON response from Vinted: {e}")
try:
self.logger.debug(f"Response text: {response.text[:500]}")
except:
pass
except Exception as e:
self.logger.error(f"Unexpected error fetching from Vinted: {e}")
if attempt < max_retries - 1:
wait_time = REQUEST_RETRY_TIME * (attempt + 1)
self.logger.info(f"Retrying in {wait_time} seconds... (attempt {attempt + 2}/{max_retries})")
time.sleep(wait_time)
self.logger.warning(f"Failed to fetch articles from Vinted after {max_retries} attempts")
return []
def parse_response(self, json_items):
"""Parsear respuesta de Vinted"""
articles = []
for json_article in json_items:
article = self._parse_single_article(json_article)
if article:
articles.append(article)
return articles
def _parse_single_article(self, json_data):
"""Parsear un artículo individual de Vinted"""
try:
# ID del artículo
article_id = str(json_data['id'])
# Título
title = json_data.get('title', '')
# Descripción
description = json_data.get('description', '')
# Precio (Vinted devuelve en céntimos, convertimos a euros)
price_amount = json_data.get('price', {}).get('amount', 0)
if price_amount:
price = float(price_amount)
else:
price = 0.0
# Moneda
currency = json_data.get('price', {}).get('currency_code', 'EUR')
# Ubicación
user_data = json_data.get('user', {})
location = user_data.get('city', 'Unknown')
# URL del artículo
article_url = json_data.get('url', '')
if article_url and not article_url.startswith('http'):
article_url = f"https://www.{self.domain}{article_url}"
# Imágenes
images = []
photo = json_data.get('photo')
if photo and 'url' in photo:
images.append(photo['url'])
# Imágenes adicionales
photos = json_data.get('photos', [])
for photo in photos[:3]: # Máximo 3 imágenes
if 'url' in photo:
url = photo['url']
if url not in images: # Evitar duplicados
images.append(url)
# Limitar a 3 imágenes
images = images[:3]
# Fecha de modificación
updated_at_str = json_data.get('photo', {}).get('high_resolution', {}).get('timestamp')
if not updated_at_str:
# Alternativa: usar created_at_ts
created_ts = json_data.get('created_at_ts')
if created_ts:
try:
dt = datetime.fromtimestamp(int(created_ts))
modified_at = dt.strftime("%Y-%m-%d %H:%M:%S")
except:
modified_at = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
else:
modified_at = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
else:
try:
dt = datetime.fromtimestamp(int(updated_at_str))
modified_at = dt.strftime("%Y-%m-%d %H:%M:%S")
except:
modified_at = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Envíos - Vinted generalmente permite envíos
allows_shipping = True
return Article(
id=article_id,
title=title,
description=description,
price=price,
currency=currency,
location=location,
allows_shipping=allows_shipping,
url=article_url,
images=images,
modified_at=modified_at,
platform=self.get_platform_name()
)
except (KeyError, ValueError, TypeError) as e:
self.logger.info(f"Error parsing Vinted article: {e} {json_data}")
return None