Skip to content

Commit b9e50b2

Browse files
committed
feat: vinted logs v2
1 parent 3bca053 commit b9e50b2

12 files changed

Lines changed: 485 additions & 208 deletions

api/images/1763681389.webp

130 KB
Loading

api/images/2046772566.jpg

2 MB
Loading

api/images/Alakazam-2.webp

132 KB
Loading

api/services/vinted_batch_orchestrator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ async def forward_progress(ev: dict[str, Any]) -> None:
6969
await VintedService.init_page()
7070
await TimerService.wait(80)
7171
await _emit_job(job_id, "auth", "Connexion à Vinted…", form_step="auth_start")
72-
await VintedService.ensure_sign_in(email, password)
72+
await VintedService.ensure_sign_in(email, password, form_progress=forward_progress)
7373
await _emit_job(job_id, "auth", "Connecté — enchaînement des annonces.", form_step="auth_ok")
7474

7575
except Exception as exc: # noqa: BLE001

api/services/vinted_publish_service.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ async def _emit(
5050
*,
5151
form_step: str | None = None,
5252
detail: str | None = None,
53+
screenshot: str | None = None,
5354
) -> None:
5455
if progress is None:
5556
return
@@ -58,6 +59,8 @@ async def _emit(
5859
ev["form_step"] = form_step
5960
if detail is not None:
6061
ev["detail"] = detail
62+
if screenshot:
63+
ev["screenshot"] = screenshot
6164
await progress(ev)
6265

6366

@@ -220,7 +223,7 @@ async def publish_article_to_vinted(
220223
await VintedService.init_page()
221224
await TimerService.wait(80)
222225
await _emit(progress, "auth", "Connexion à Vinted…", form_step="auth_start")
223-
await VintedService.ensure_sign_in(email, password)
226+
await VintedService.ensure_sign_in(email, password, form_progress=progress)
224227
await _emit(progress, "auth", "Connecté.", form_step="auth_ok")
225228
await run_single_vinted_listing(article, user, stored_image_sources, progress)
226229
await _emit(progress, "browser", "Fermeture du navigateur…", form_step="browser_close")

api/services/vinted_service.py

Lines changed: 201 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@
33
from __future__ import annotations
44

55
import asyncio
6+
import base64
67
import json
78
import logging
9+
import os
10+
import tempfile
811
import time
912
from collections.abc import Awaitable, Callable
13+
from pathlib import Path
1014
from typing import TYPE_CHECKING, Any, Optional
1115

1216
import nodriver as uc
@@ -120,6 +124,62 @@ class VintedService:
120124
_tab: Optional[Tab] = None
121125
_VINTED_TIMEOUT_MS: int = 350
122126

127+
@classmethod
128+
async def _emit_auth_log(
129+
cls,
130+
form_progress: FormProgressFn | None,
131+
form_step: str,
132+
message: str,
133+
tab: Tab | None = None,
134+
*,
135+
detail: str | None = None,
136+
with_screenshot: bool = False,
137+
) -> None:
138+
"""Événements SSE pendant l’auth Vinted (sous-étapes + capture optionnelle)."""
139+
if not form_progress:
140+
return
141+
ev: dict[str, Any] = {
142+
"type": "log",
143+
"step": "auth",
144+
"form_step": form_step,
145+
"message": message,
146+
}
147+
if detail is not None:
148+
ev["detail"] = detail
149+
if with_screenshot and tab is not None:
150+
shot = await cls._tab_screenshot_data_url(tab)
151+
if shot:
152+
ev["screenshot"] = shot
153+
await form_progress(ev)
154+
155+
@classmethod
156+
async def _tab_screenshot_data_url(cls, tab: Tab) -> str | None:
157+
"""JPEG redimensionné (data URL) pour les logs SSE — évite les payloads énormes."""
158+
try:
159+
fd, path = tempfile.mkstemp(suffix=".jpg")
160+
os.close(fd)
161+
p = Path(path)
162+
await tab.save_screenshot(str(p), format="jpeg")
163+
raw = p.read_bytes()
164+
p.unlink(missing_ok=True)
165+
try:
166+
from io import BytesIO
167+
168+
from PIL import Image
169+
170+
im = Image.open(BytesIO(raw)).convert("RGB")
171+
im.thumbnail((1280, 1280))
172+
buf = BytesIO()
173+
im.save(buf, format="JPEG", quality=72, optimize=True)
174+
raw = buf.getvalue()
175+
except Exception:
176+
pass
177+
b64 = base64.b64encode(raw).decode("ascii")
178+
return f"data:image/jpeg;base64,{b64}"
179+
except Exception as exc: # noqa: BLE001
180+
logger.warning("Vinted screenshot failed: %s", exc)
181+
return None
182+
123183
@classmethod
124184
async def init_browser(cls) -> None:
125185
"""
@@ -568,7 +628,12 @@ async def _activate_se_connecter_switch(cls, tab: Tab, el: Element) -> None:
568628
await cls._activate_auth_testid_span(tab, el, "auth-select-type--register-switch")
569629

570630
@classmethod
571-
async def _click_auth_se_connecter_switch(cls, tab: Tab, total_timeout_sec: float = 45.0) -> None:
631+
async def _click_auth_se_connecter_switch(
632+
cls,
633+
tab: Tab,
634+
total_timeout_sec: float = 45.0,
635+
form_progress: FormProgressFn | None = None,
636+
) -> None:
572637
"""
573638
On the auth screen, click "Se connecter" to switch from the sign-up panel to the login panel.
574639
@@ -588,6 +653,13 @@ async def _click_auth_se_connecter_switch(cls, tab: Tab, total_timeout_sec: floa
588653

589654
if await cls._login_username_field_is_visible(tab):
590655
logger.info("Login panel already visible; skipping %s.", selector)
656+
await cls._emit_auth_log(
657+
form_progress,
658+
"auth_skip_se_connecter",
659+
"Panneau connexion déjà affiché (champ identifiant visible).",
660+
tab,
661+
detail=(tab.target.url or "")[:400],
662+
)
591663
return
592664

593665
wait_timeout = min(5.0, max(1.2, min(total_timeout_sec, 8.0)))
@@ -610,23 +682,51 @@ async def _click_auth_se_connecter_switch(cls, tab: Tab, total_timeout_sec: floa
610682

611683
await cls._activate_se_connecter_switch(tab, el)
612684
logger.info("Activated Se connecter (%s).", selector)
685+
await cls._emit_auth_log(
686+
form_progress,
687+
"auth_se_connecter_clicked",
688+
"« Se connecter » activé — attente du champ identifiant (max 4 s).",
689+
tab,
690+
detail=(tab.target.url or "")[:400],
691+
)
613692
await cls._scroll_page_top(tab)
693+
await tab.sleep(0.12)
614694

615-
settle_deadline = time.monotonic() + 1.5
695+
settle_deadline = time.monotonic() + 4.0
616696
while time.monotonic() < settle_deadline:
617697
await tab
618698
if await cls._login_username_field_is_visible(tab):
619699
logger.info("Login username field is now visible.")
700+
await cls._emit_auth_log(
701+
form_progress,
702+
"auth_username_visible",
703+
"Champ identifiant visible après « Se connecter ».",
704+
tab,
705+
detail=(tab.target.url or "")[:400],
706+
)
620707
return
621-
await asyncio.sleep(0.05)
708+
await asyncio.sleep(0.06)
622709

623710
if not await cls._login_username_field_is_visible(tab):
624711
logger.warning(
625-
"Se connecter was activated but username field did not become visible within 1.5s.",
712+
"Se connecter was activated but username field did not become visible within 4s.",
713+
)
714+
await cls._emit_auth_log(
715+
form_progress,
716+
"auth_username_wait",
717+
"Champ identifiant toujours absent après attente — capture d'écran.",
718+
tab,
719+
detail=(tab.target.url or "")[:500],
720+
with_screenshot=True,
626721
)
627722

628723
@classmethod
629-
async def _click_auth_login_email_option(cls, tab: Tab, total_timeout_sec: float = 12.0) -> None:
724+
async def _click_auth_login_email_option(
725+
cls,
726+
tab: Tab,
727+
total_timeout_sec: float = 12.0,
728+
form_progress: FormProgressFn | None = None,
729+
) -> None:
630730
"""
631731
After "Se connecter", choose login with e-mail (``auth-select-type--login-email``).
632732
"""
@@ -635,6 +735,13 @@ async def _click_auth_login_email_option(cls, tab: Tab, total_timeout_sec: float
635735
await tab
636736
if await cls._login_username_field_is_visible(tab):
637737
logger.info("Username field already visible; skipping e-mail login method picker.")
738+
await cls._emit_auth_log(
739+
form_progress,
740+
"auth_skip_login_email",
741+
"Connexion par e-mail inutile — champ identifiant déjà visible.",
742+
tab,
743+
detail=(tab.target.url or "")[:400],
744+
)
638745
return
639746

640747
wait_timeout = min(5.0, max(1.0, min(total_timeout_sec, 8.0)))
@@ -653,11 +760,25 @@ async def _click_auth_login_email_option(cls, tab: Tab, total_timeout_sec: float
653760

654761
await cls._activate_auth_testid_span(tab, el, testid)
655762
logger.info("Clicked login via e-mail (%s).", selector)
763+
await cls._emit_auth_log(
764+
form_progress,
765+
"auth_login_email_clicked",
766+
"Option « Connexion par e-mail » activée.",
767+
tab,
768+
detail=(tab.target.url or "")[:400],
769+
)
656770
await cls._scroll_page_top(tab)
657-
await tab.sleep(0.05)
771+
await tab.sleep(0.12)
658772

659773
@classmethod
660-
async def _fill_and_submit_credentials(cls, tab: Tab, email: str, password: str) -> None:
774+
async def _fill_and_submit_credentials(
775+
cls,
776+
tab: Tab,
777+
email: str,
778+
password: str,
779+
*,
780+
form_progress: FormProgressFn | None = None,
781+
) -> None:
661782
"""
662783
Fill username/password and submit the member login form (Continuer).
663784
@@ -669,12 +790,34 @@ async def _fill_and_submit_credentials(cls, tab: Tab, email: str, password: str)
669790
while time.monotonic() < form_deadline:
670791
await tab
671792
if await cls._login_username_field_is_visible(tab):
793+
await cls._emit_auth_log(
794+
form_progress,
795+
"auth_username_ready",
796+
"Champ identifiant visible — remplissage des identifiants.",
797+
tab,
798+
detail=(tab.target.url or "")[:400],
799+
)
672800
break
673-
await asyncio.sleep(0.06)
801+
await asyncio.sleep(0.08)
674802
else:
675803
logger.warning("Username field did not become visible before credential fill.")
804+
await cls._emit_auth_log(
805+
form_progress,
806+
"auth_username_missing",
807+
"Champ identifiant non visible avant saisie — capture d'écran.",
808+
tab,
809+
detail=(tab.target.url or "")[:500],
810+
with_screenshot=True,
811+
)
676812

677813
await cls._scroll_page_top(tab)
814+
await cls._emit_auth_log(
815+
form_progress,
816+
"auth_fill_start",
817+
"Saisie de l'identifiant / mot de passe…",
818+
tab,
819+
detail="Champs username + password",
820+
)
678821
if not await cls._set_input_value_for_react(tab, "input#username", email):
679822
await cls._set_input_value_for_react(tab, 'input[name="username"]', email)
680823
await tab.sleep(0.05)
@@ -715,6 +858,14 @@ async def _fill_and_submit_credentials(cls, tab: Tab, email: str, password: str)
715858
return_by_value=True,
716859
)
717860
logger.info("Submitted login via JS (Continuer / first submit).")
861+
await cls._emit_auth_log(
862+
form_progress,
863+
"auth_submit_sent",
864+
"Formulaire de connexion envoyé (Continuer / submit).",
865+
tab,
866+
detail=(tab.target.url or "")[:400],
867+
with_screenshot=True,
868+
)
718869
await tab.sleep(0.6)
719870
await tab
720871

@@ -741,7 +892,13 @@ async def is_connected(cls) -> bool:
741892
return True
742893

743894
@classmethod
744-
async def ensure_sign_in(cls, email: str, password: str) -> None:
895+
async def ensure_sign_in(
896+
cls,
897+
email: str,
898+
password: str,
899+
*,
900+
form_progress: FormProgressFn | None = None,
901+
) -> None:
745902
"""
746903
Attempt sign-in up to five times using :meth:`sign_in_vinted`.
747904
@@ -756,7 +913,7 @@ async def ensure_sign_in(cls, email: str, password: str) -> None:
756913
RuntimeError: If sign-in is still considered unsuccessful after all attempts.
757914
"""
758915
max_attempts = 5
759-
await cls.sign_in_vinted(email, password, from_home=True)
916+
await cls.sign_in_vinted(email, password, from_home=True, form_progress=form_progress)
760917
await cls._require_tab().sleep(1.8)
761918
if await cls.is_connected():
762919
logger.info(
@@ -770,7 +927,7 @@ async def ensure_sign_in(cls, email: str, password: str) -> None:
770927
attempt,
771928
max_attempts - 1,
772929
)
773-
await cls.sign_in_vinted(email, password, from_home=False)
930+
await cls.sign_in_vinted(email, password, from_home=False, form_progress=form_progress)
774931
await cls._require_tab().sleep(1.5)
775932
if await cls.is_connected():
776933
logger.info(
@@ -781,7 +938,14 @@ async def ensure_sign_in(cls, email: str, password: str) -> None:
781938
raise RuntimeError("Failed to sign in after multiple attempts")
782939

783940
@classmethod
784-
async def sign_in_vinted(cls, email: str, password: str, *, from_home: bool = True) -> None:
941+
async def sign_in_vinted(
942+
cls,
943+
email: str,
944+
password: str,
945+
*,
946+
from_home: bool = True,
947+
form_progress: FormProgressFn | None = None,
948+
) -> None:
785949
"""
786950
Navigate to auth, click Se connecter → e-mail, fill credentials, submit.
787951
@@ -796,22 +960,44 @@ async def sign_in_vinted(cls, email: str, password: str, *, from_home: bool = Tr
796960
"""
797961
tab = cls._require_tab()
798962
if from_home:
963+
await cls._emit_auth_log(
964+
form_progress,
965+
"auth_nav_home",
966+
"Ouverture de vinted.fr (accueil)…",
967+
tab,
968+
detail="https://www.vinted.fr",
969+
)
799970
await tab.get("https://www.vinted.fr")
800971
await cls._accept_onetrust_cookies(tab)
801972
await TimerService.wait(40)
802973
await cls._go_to_member_auth_entry(tab)
974+
await cls._emit_auth_log(
975+
form_progress,
976+
"auth_member_entry",
977+
"Page d'authentification membre chargée.",
978+
tab,
979+
detail=(tab.target.url or "")[:500],
980+
with_screenshot=True,
981+
)
803982
else:
983+
await cls._emit_auth_log(
984+
form_progress,
985+
"auth_retry_url",
986+
"Rechargement direct de l'URL de connexion membre (retry).",
987+
tab,
988+
detail=VINTED_MEMBER_AUTH_URL,
989+
)
804990
await tab.get(VINTED_MEMBER_AUTH_URL)
805991
await tab
806992
await tab.sleep(0.08)
807993

808994
await cls._accept_onetrust_cookies(tab, total_timeout_sec=4.0)
809995
await TimerService.wait(30)
810996
await cls._scroll_page_top(tab)
811-
await cls._click_auth_se_connecter_switch(tab, total_timeout_sec=12.0)
997+
await cls._click_auth_se_connecter_switch(tab, total_timeout_sec=12.0, form_progress=form_progress)
812998
await tab.sleep(0.04)
813-
await cls._click_auth_login_email_option(tab, total_timeout_sec=8.0)
814-
await cls._fill_and_submit_credentials(tab, email, password)
999+
await cls._click_auth_login_email_option(tab, total_timeout_sec=8.0, form_progress=form_progress)
1000+
await cls._fill_and_submit_credentials(tab, email, password, form_progress=form_progress)
8151001

8161002
@classmethod
8171003
async def open_sell_item_page(cls) -> None:

todo.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
=- sync vinted
22

3+
ajouter un input si l'ocr détecte mauvaise carte pour aider un peu pokewallter à remplir le formulaire create articles avec les infos de la bonne carte
4+
35
app.goupixdex
46

57
goupixdex à la racine landing qui présente le saas goupix dex avec cta pour faire sa demande d'accès avec un email

0 commit comments

Comments
 (0)