11"""
2- TODO :
3- - add a input front to select the number of n last ads to refresh
4- - finish the refresh ads function
2+ Notes :
3+ - Vinted uses SSR and item data is hidden in html, its a pain to extract and I did not manage
4+ They are building an API for pro users: https://pro-docs.svc.vinted.com/#vinted-pro-integrations-documentation-items-api
55"""
66
77import json
88import requests
99import re
10- from bs4 import BeautifulSoup
1110from time import sleep
12- from typing import List
1311from fastapi import APIRouter , Depends
14- from pydantic import BaseModel
1512from utils import execute_request , get_vinted_headers
1613from constants import API_URL
1714from sqlmodel import Session , select
2219)
2320
2421
25- class PhotoThumbnail (BaseModel ):
26- type : str
27- url : str
28-
29-
30- class Photo (BaseModel ):
31- thumbnails : List [PhotoThumbnail ]
32-
33-
34- class Price (BaseModel ):
35- currency_code : str
36- amount : float
37-
38-
39- def extract_photos_urls (photos : List [Photo ]) -> List [str ]:
40- """Extracts photo URLs from photos data"""
41- urls = []
42- for photo in photos :
43- for thumbnail in photo .thumbnails :
44- if thumbnail .type == "thumb364x428" :
45- urls .append (thumbnail .url )
46- return urls
47-
48-
49- def prepare_item_info (item : dict ) -> dict :
50- pass
51-
52-
53- async def upload_photos (urls : List [str ], temp_uuid : str , headers : dict ) -> List [int ]:
54- """Uploads photos and returns list of photo IDs"""
55- photo_ids = []
56-
57- for i , url in enumerate (urls ):
58- response = requests .get (url )
59- if response .status_code != 200 :
60- continue
61-
62- files = [("photo[file]" , (f"{ i } .jpg" , response .content , "image/jpeg" ))]
63- payload = {"photo[type]" : "item" , "photo[temp_uuid]" : temp_uuid }
64-
65- response = requests .post (
66- "https://www.vinted.fr/api/v2/photos" ,
67- headers = headers ,
68- data = payload ,
69- files = files ,
70- )
71-
72- if response .status_code == 200 :
73- photo_ids .append (response .json ()["id" ])
74-
75- return photo_ids
76-
77-
7822@router .get ("/refresh-ads" )
7923async def refresh_ads (
8024 headers : dict = Depends (get_vinted_headers ), session : Session = Depends (get_session )
8125):
8226 """Refreshes ads available for the authenticated profile"""
83- same_row = 0
8427 page = 1
8528
8629 url = f"{ API_URL } wardrobe/4977264403/items?page={ page } &per_page=20&order=revelance"
8730 response = execute_request ("GET" , url , headers )
8831
8932 total_pages = response .json ()["pagination" ]["total_pages" ]
9033
91- print ("Il y a" , total_pages , "pages" )
92-
9334 while page <= total_pages :
9435 user = session .exec (select (User ).where (User .id == 1 )).first ()
9536 url = f"{ API_URL } wardrobe/{ user .userId } /items?page={ page } &per_page=20&order=revelance"
@@ -114,65 +55,32 @@ async def refresh_ads(
11455
11556 response = execute_request ("GET" , item_url , headers )
11657
117- result = re .search (
118- r'(\{"itemDto":.*?"electronicsVerification":.*?\}\})\]' ,
119- response .text .replace ('\\ "' , '"' ).replace ("\\ \\ n" , "\\ n" ),
120- )
121- if result :
122- item_dto_str = result .group (1 )
123-
124- try :
125- item_data = json .loads (item_dto_str )
126- except json .JSONDecodeError as e :
127- print (f"Error parsing JSON: { e } " )
58+ item_data = extract_item_json (response .text )
59+
60+ if item_data :
61+ return {"message" : "Not implemented" }
62+
63+ """
64+ Roadmap to repost an item:
65+ 1) Download current photos
66+ 2) Upload new photos
67+ 3) Delete item
68+ 4) Upload item
69+ """
70+
71+ # 3) Delete item before reposting it
72+ if delete_item (item ["path" ], item ["id" ], headers ):
73+ print (f"Item: { item_data ['title' ]} deleted successfully" )
74+ else :
75+ print (f"Item: { item_data ['title' ]} not deleted, skipping" )
76+ continue
77+
78+ # 4) Upload item
79+
80+ print (f"Item: { item_data ['title' ]} reposted successfully" )
12881 else :
129- print ("No item data found in the response" )
130-
131- return {
132- "message" : "Item data fetched successfully" ,
133- }
134-
135- # Prepare item data
136- # item_info = prepare_item_info(item)
137- # photo_urls = extract_photos_urls(item["photos"])
138-
139- # # Handle rate limiting
140- # if same_row == 15:
141- # sleep(30)
142- # same_row = 0
143-
144- # # Get temp UUID for upload
145- # response = requests.get("https://www.vinted.fr/items/new", headers=headers)
146- # temp_uuid = (
147- # re.search(
148- # r'<div id="ItemUpload-react-component-\s*(.*?)\s*"',
149- # response.text,
150- # re.DOTALL,
151- # )
152- # .group(1)
153- # .strip()
154- # )
155-
156- # # Upload photos
157- # photo_ids = await upload_photos(photo_urls, temp_uuid, headers)
158-
159- # # Prepare final item data
160- # item_info["temp_uuid"] = temp_uuid
161- # item_info["assigned_photos"] = [
162- # {"id": pid, "orientation": 0} for pid in photo_ids
163- # ]
164-
165- # Upload item
166- # response = requests.post(
167- # "https://www.vinted.fr/api/v2/items",
168- # headers=headers,
169- # json={"item": item_info, "feedback_id": None},
170- # )
171-
172- # if response.status_code == 200:
173- # # Delete old item
174- # requests.post(f"{API_URL}items/{item['id']}/delete", headers=headers)
175- # same_row += 1
82+ print ("No item data found in the response, skipping" )
83+ continue
17684
17785 page += 1
17886
@@ -183,7 +91,6 @@ async def refresh_ads(
18391async def delete_sold_items (
18492 headers : dict = Depends (get_vinted_headers ), session : Session = Depends (get_session )
18593):
186- """Deletes all sold items"""
18794 page = 1
18895
18996 while True :
@@ -211,10 +118,7 @@ async def delete_sold_items(
211118 sleep (30 )
212119 nb_items_deleted = 0
213120
214- response = execute_request (
215- "delete" , f"{ API_URL } items/{ item ['id' ]} /delete" , headers
216- )
217- if response .status_code == 200 :
121+ if delete_item (item ["path" ], item ["id" ], headers ):
218122 nb_items_deleted += 1
219123
220124 page += 1
@@ -226,7 +130,6 @@ async def delete_sold_items(
226130async def delete_all_ads (
227131 headers : dict = Depends (get_vinted_headers ), session : Session = Depends (get_session )
228132):
229- """Deletes all ads"""
230133 page = 1
231134
232135 while True :
@@ -250,12 +153,94 @@ async def delete_all_ads(
250153 sleep (30 )
251154 nb_items_deleted = 0
252155
253- response = execute_request (
254- "delete" , f"{ API_URL } items/{ item ['id' ]} /delete" , headers
255- )
256- if response .status_code == 200 :
156+ if delete_item (item ["path" ], item ["id" ], headers ):
257157 nb_items_deleted += 1
258158
259159 page += 1
260160
261161 return {"message" : "All ads deleted" }
162+
163+
164+ def delete_item (path : str , item_id : int , headers : dict ) -> bool :
165+ item_url = f"https://www.vinted.fr{ path } "
166+
167+ csrf_token = get_csrf_token (item_url , headers )
168+ if csrf_token :
169+ headers ["X-CSRF-Token" ] = csrf_token
170+ response = execute_request (
171+ "DELETE" , f"{ API_URL } items/{ item_id } /delete" , headers
172+ )
173+ return response .status_code == 200
174+ else :
175+ return False
176+
177+
178+ def get_csrf_token (item_url : str , headers : dict ) -> str | None :
179+ response = requests .get (item_url , headers = headers )
180+
181+ if response .status_code == 200 :
182+ html_content = response .text
183+ pattern = r'\\"CSRF_TOKEN\\":\\"(.*?)\\"'
184+
185+ matches = re .findall (pattern , html_content )
186+ if matches :
187+ return matches [0 ]
188+ return None
189+ else :
190+ return None
191+
192+
193+ def extract_item_json (html_content : str ):
194+ """Vinted loves to hide its data in html, this is how to extract with chatGPT"""
195+ # Find all script tags with the pattern
196+ script_matches = re .findall (
197+ r'self\.__next_f\.push\(\[1,"(.*?)"\]\)</script>' , html_content , re .DOTALL
198+ )
199+
200+ for json_str in script_matches :
201+ if json_str .startswith ("16:" ):
202+ json_str = json_str [3 :]
203+
204+ json_str = json_str .replace ('\\ "' , '"' ).replace ("\\ \\ n" , "\\ n" )
205+
206+ # Check if this script contains item_closing_action
207+ if "item_closing_action" in json_str :
208+ # Find the first complete JSON object
209+ brace_count = 0
210+ start = json_str .find ("{" )
211+ if start != - 1 :
212+ for i in range (start , len (json_str )):
213+ if json_str [i ] == "{" :
214+ brace_count += 1
215+ elif json_str [i ] == "}" :
216+ brace_count -= 1
217+ if brace_count == 0 :
218+ json_str = json_str [start : i + 1 ]
219+ break
220+
221+ try :
222+ data = json .loads (json_str )
223+
224+ # Find item in the structure
225+ def find_item (obj ):
226+ if isinstance (obj , dict ) and "item" in obj :
227+ return obj ["item" ]
228+ elif isinstance (obj , dict ):
229+ for v in obj .values ():
230+ result = find_item (v )
231+ if result :
232+ return result
233+ elif isinstance (obj , list ):
234+ for v in obj :
235+ result = find_item (v )
236+ if result :
237+ return result
238+ return None
239+
240+ return find_item (data )
241+ except json .JSONDecodeError as e :
242+ print (f"Error parsing JSON: { e } " )
243+ continue
244+
245+ print ("No script with item_closing_action found" )
246+ return None
0 commit comments