abdulsalam2121 commited on
Commit
91cff05
·
1 Parent(s): 0623847

manage list correctly

Browse files
Files changed (4) hide show
  1. app/bot.py +13 -7
  2. app/main.py +12 -9
  3. app/modify_listings_updater.py +318 -0
  4. app/services/run_bot.py +26 -10
app/bot.py CHANGED
@@ -1,10 +1,10 @@
1
- import time
2
  import logging
3
  from typing import Optional
4
 
5
  from config import BotConfig, HARDCODED_USERNAME, HARDCODED_PASSWORD
6
  from browser_session import BrowserSession
7
  from auth_handler import AuthHandler
 
8
 
9
  import queue
10
 
@@ -70,12 +70,18 @@ class BotRunner:
70
  self._log("Failed to open target page")
71
  return
72
 
73
- self._log("Phase 3 Browser active click Stop to close")
74
- while True:
75
- if self.stop_event and self.stop_event.is_set():
76
- self._log("Stopped by user")
77
- break
78
- time.sleep(0.5)
 
 
 
 
 
 
79
 
80
  except Exception as e:
81
  logger.exception("Unexpected error in BotRunner")
 
 
1
  import logging
2
  from typing import Optional
3
 
4
  from config import BotConfig, HARDCODED_USERNAME, HARDCODED_PASSWORD
5
  from browser_session import BrowserSession
6
  from auth_handler import AuthHandler
7
+ from modify_listings_updater import update_wo_case_listing_prices
8
 
9
  import queue
10
 
 
70
  self._log("Failed to open target page")
71
  return
72
 
73
+ self._log("Phase 3 - Updating w/o case listing prices")
74
+ result = update_wo_case_listing_prices(
75
+ session.page,
76
+ dashboard_url=self.target_url,
77
+ stop_event=self.stop_event,
78
+ )
79
+ self._log(
80
+ "Update complete: "
81
+ f"matched={result['matched']}, updated={result['updated']}, "
82
+ f"failed={result['failed']}, pages={result['pages_scanned']}"
83
+ )
84
+ self._log(f"CSV saved: {result['csv_path']}")
85
 
86
  except Exception as e:
87
  logger.exception("Unexpected error in BotRunner")
app/main.py CHANGED
@@ -7,13 +7,13 @@ Usage:
7
  """
8
  import argparse
9
  import sys
10
- import time
11
  from pathlib import Path
12
 
13
  from config import BotConfig, DEFAULT_TARGET_URL, HARDCODED_USERNAME, HARDCODED_PASSWORD
14
  from logger_setup import setup_logger
15
  from browser_session import BrowserSession
16
  from auth_handler import AuthHandler
 
17
 
18
 
19
  def build_parser() -> argparse.ArgumentParser:
@@ -76,14 +76,17 @@ def main() -> None:
76
  logger.error("Failed to open target page — aborting")
77
  sys.exit(1)
78
 
79
- # Keep browser open for user to interact
80
- logger.info("Browser session active. Press Ctrl+C to exit.")
81
- print("\n Press Ctrl+C in this terminal to close the browser.")
82
- try:
83
- while True:
84
- time.sleep(1)
85
- except KeyboardInterrupt:
86
- logger.info("Interrupted by user")
 
 
 
87
 
88
  except Exception as exc:
89
  logger.error(f"Unexpected error: {exc}", exc_info=True)
 
7
  """
8
  import argparse
9
  import sys
 
10
  from pathlib import Path
11
 
12
  from config import BotConfig, DEFAULT_TARGET_URL, HARDCODED_USERNAME, HARDCODED_PASSWORD
13
  from logger_setup import setup_logger
14
  from browser_session import BrowserSession
15
  from auth_handler import AuthHandler
16
+ from modify_listings_updater import update_wo_case_listing_prices
17
 
18
 
19
  def build_parser() -> argparse.ArgumentParser:
 
76
  logger.error("Failed to open target page — aborting")
77
  sys.exit(1)
78
 
79
+ logger.info("")
80
+ logger.info("Phase 3 - Updating w/o case listing prices")
81
+ result = update_wo_case_listing_prices(session.page, dashboard_url=args.url)
82
+ logger.info(
83
+ "Update complete: matched=%s, updated=%s, failed=%s, pages=%s",
84
+ result["matched"],
85
+ result["updated"],
86
+ result["failed"],
87
+ result["pages_scanned"],
88
+ )
89
+ logger.info("CSV saved: %s", result["csv_path"])
90
 
91
  except Exception as exc:
92
  logger.error(f"Unexpected error: {exc}", exc_info=True)
app/modify_listings_updater.py ADDED
@@ -0,0 +1,318 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import csv
2
+ import logging
3
+ import re
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+ from typing import Any, Callable, Dict, List, Optional
7
+ from urllib.parse import urljoin
8
+
9
+ from playwright.sync_api import Page, TimeoutError as PlaywrightTimeoutError
10
+
11
+ from config import DEFAULT_TARGET_URL
12
+
13
+ logger = logging.getLogger("bot")
14
+
15
+ CSV_HEADERS = ["Timestamp", "Listing ID", "Item Title", "Old Price", "New Price"]
16
+ ROW_SELECTOR = "table.table-striped tbody tr"
17
+ TARGET_MARKER = "w/o case"
18
+ TARGET_PRICE = "9.95"
19
+
20
+
21
+ def _ensure_csv(csv_path: Path) -> None:
22
+ csv_path.parent.mkdir(parents=True, exist_ok=True)
23
+ if not csv_path.exists() or csv_path.stat().st_size == 0:
24
+ with csv_path.open("w", newline="", encoding="utf-8") as csv_file:
25
+ csv.writer(csv_file).writerow(CSV_HEADERS)
26
+
27
+
28
+ def _append_change(csv_path: Path, listing: Dict[str, str], new_price: str) -> None:
29
+ with csv_path.open("a", newline="", encoding="utf-8") as csv_file:
30
+ csv.writer(csv_file).writerow(
31
+ [
32
+ datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
33
+ listing["listing_id"],
34
+ listing["title"],
35
+ listing["old_price"],
36
+ new_price,
37
+ ]
38
+ )
39
+
40
+
41
+ def _clean_title(cell_text: str) -> str:
42
+ lines = [line.strip() for line in cell_text.splitlines() if line.strip()]
43
+ skipped_patterns = (
44
+ re.compile(r"edit\s+my\s+listing", re.IGNORECASE),
45
+ re.compile(r"\bcount\s*:", re.IGNORECASE),
46
+ re.compile(r"\bw/o\s+case\b", re.IGNORECASE),
47
+ )
48
+
49
+ for line in lines:
50
+ if not any(pattern.search(line) for pattern in skipped_patterns):
51
+ return line
52
+
53
+ cleaned = re.sub(r"edit\s+my\s+listing", "", cell_text, flags=re.IGNORECASE)
54
+ cleaned = re.sub(r"count\s*:\s*\d+.*", "", cleaned, flags=re.IGNORECASE)
55
+ return " ".join(cleaned.split()) or "Unknown Title"
56
+
57
+
58
+ def _extract_price(cell_text: str, fallback_value: str = "") -> str:
59
+ match = re.search(r"\$\s*[\d,]+(?:\.\d{1,2})?", cell_text)
60
+ if match:
61
+ return match.group(0).replace(" ", "")
62
+
63
+ match = re.search(r"[\d,]+(?:\.\d{1,2})", cell_text)
64
+ if match:
65
+ return match.group(0)
66
+
67
+ return fallback_value.strip()
68
+
69
+
70
+ def _extract_listing_id(name_value: str) -> Optional[str]:
71
+ match = re.search(r"(?:price|selectcb)_(\d+)", name_value or "")
72
+ return match.group(1) if match else None
73
+
74
+
75
+ def _wait_for_dashboard(page: Page) -> None:
76
+ page.wait_for_selector(ROW_SELECTOR, state="attached", timeout=30000)
77
+
78
+
79
+ def _scan_current_dashboard_page(page: Page) -> List[Dict[str, str]]:
80
+ _wait_for_dashboard(page)
81
+ rows = page.locator(ROW_SELECTOR)
82
+ matched: List[Dict[str, str]] = []
83
+
84
+ for index in range(rows.count()):
85
+ row = rows.nth(index)
86
+ cells = row.locator("td")
87
+
88
+ if cells.count() < 4:
89
+ continue
90
+
91
+ title_cell = cells.nth(1)
92
+ price_cell = cells.nth(3)
93
+ title_cell_text = title_cell.inner_text(timeout=3000)
94
+
95
+ if TARGET_MARKER not in title_cell_text.lower():
96
+ continue
97
+
98
+ price_input = price_cell.locator("input[type='text'][name^='price_']").first
99
+ price_input_name = ""
100
+ price_input_value = ""
101
+ try:
102
+ price_input_name = price_input.get_attribute("name", timeout=1000) or ""
103
+ price_input_value = price_input.get_attribute("value", timeout=1000) or ""
104
+ except Exception:
105
+ pass
106
+
107
+ listing_id = _extract_listing_id(price_input_name)
108
+ if not listing_id:
109
+ checkbox = row.locator("input[name^='selectcb_']").first
110
+ try:
111
+ listing_id = _extract_listing_id(checkbox.get_attribute("name", timeout=1000) or "")
112
+ except Exception:
113
+ listing_id = None
114
+
115
+ edit_link = title_cell.locator("a:has-text('Edit My Listing')").first
116
+ try:
117
+ edit_href = edit_link.get_attribute("href", timeout=2000) or ""
118
+ except Exception:
119
+ edit_href = ""
120
+
121
+ if not listing_id or not edit_href:
122
+ logger.warning("Skipping matched row %s because listing id or edit href is missing", index + 1)
123
+ continue
124
+
125
+ matched.append(
126
+ {
127
+ "listing_id": listing_id,
128
+ "title": _clean_title(title_cell_text),
129
+ "old_price": _extract_price(price_cell.inner_text(timeout=3000), price_input_value),
130
+ "edit_url": urljoin(page.url, edit_href),
131
+ }
132
+ )
133
+
134
+ logger.info("Found %s matching listing(s) on current dashboard page", len(matched))
135
+ return matched
136
+
137
+
138
+ def _click_value_input(page: Page, label_pattern: re.Pattern) -> bool:
139
+ inputs = page.locator("input[type='submit'], input[type='button']")
140
+ for index in range(inputs.count()):
141
+ candidate = inputs.nth(index)
142
+ try:
143
+ value = candidate.get_attribute("value", timeout=1000) or ""
144
+ if label_pattern.search(value) and candidate.is_visible(timeout=1000) and candidate.is_enabled(timeout=1000):
145
+ candidate.click()
146
+ return True
147
+ except Exception:
148
+ continue
149
+ return False
150
+
151
+
152
+ def _click_control(page: Page, label: str) -> None:
153
+ label_pattern = re.compile(re.escape(label), re.IGNORECASE)
154
+
155
+ candidates = [
156
+ page.get_by_role("button", name=label_pattern),
157
+ page.get_by_role("link", name=label_pattern),
158
+ page.locator("button").filter(has_text=label_pattern),
159
+ page.locator("a").filter(has_text=label_pattern),
160
+ ]
161
+
162
+ for locator in candidates:
163
+ candidate = locator.first
164
+ try:
165
+ if candidate.is_visible(timeout=2000) and candidate.is_enabled(timeout=2000):
166
+ candidate.click()
167
+ return
168
+ except Exception:
169
+ continue
170
+
171
+ if _click_value_input(page, label_pattern):
172
+ return
173
+
174
+ raise RuntimeError(f"Could not find clickable control labeled '{label}'")
175
+
176
+
177
+ def _wait_after_click(page: Page) -> None:
178
+ try:
179
+ page.wait_for_load_state("networkidle", timeout=15000)
180
+ except PlaywrightTimeoutError:
181
+ page.wait_for_load_state("domcontentloaded", timeout=15000)
182
+
183
+
184
+ def _process_listing(page: Page, listing: Dict[str, str], new_price: str) -> None:
185
+ logger.info("Updating listing %s: %s", listing["listing_id"], listing["title"])
186
+
187
+ page.goto(listing["edit_url"], wait_until="domcontentloaded", timeout=30000)
188
+ _wait_after_click(page)
189
+
190
+ _click_control(page, "Update Your Listing")
191
+ _wait_after_click(page)
192
+
193
+ price_input = page.locator(f"input[name='price_{listing['listing_id']}']").first
194
+ price_input.wait_for(state="visible", timeout=30000)
195
+ price_input.fill("")
196
+ price_input.fill(new_price)
197
+
198
+ _click_control(page, "Update Changed Prices")
199
+ _wait_after_click(page)
200
+
201
+
202
+ def _is_disabled(locator) -> bool:
203
+ try:
204
+ aria_disabled = (locator.get_attribute("aria-disabled", timeout=500) or "").lower()
205
+ class_name = (locator.get_attribute("class", timeout=500) or "").lower()
206
+ parent_class = (locator.locator("xpath=ancestor::*[self::li or self::button][1]").get_attribute("class", timeout=500) or "").lower()
207
+ return aria_disabled == "true" or "disabled" in class_name or "disabled" in parent_class
208
+ except Exception:
209
+ return False
210
+
211
+
212
+ def _click_next_page(page: Page) -> bool:
213
+ candidates = [
214
+ page.locator("a[rel='next'], button[rel='next']"),
215
+ page.get_by_role("link", name=re.compile(r"^\s*(next|>)\s*$", re.IGNORECASE)),
216
+ page.get_by_role("button", name=re.compile(r"^\s*(next|>)\s*$", re.IGNORECASE)),
217
+ page.locator("a, button").filter(has_text=re.compile(r"^\s*(next|>)\s*$", re.IGNORECASE)),
218
+ ]
219
+
220
+ for locator in candidates:
221
+ for index in range(locator.count()):
222
+ candidate = locator.nth(index)
223
+ try:
224
+ if candidate.is_visible(timeout=1000) and candidate.is_enabled(timeout=1000) and not _is_disabled(candidate):
225
+ candidate.click()
226
+ _wait_after_click(page)
227
+ _wait_for_dashboard(page)
228
+ return True
229
+ except Exception:
230
+ continue
231
+
232
+ return False
233
+
234
+
235
+ def update_wo_case_listing_prices(
236
+ page: Page,
237
+ dashboard_url: str = DEFAULT_TARGET_URL,
238
+ target_price: str = TARGET_PRICE,
239
+ csv_path: str = "changed_listings.csv",
240
+ max_pages: int = 1000,
241
+ stop_event: Any = None,
242
+ status_callback: Optional[Callable[[Dict[str, Any]], None]] = None,
243
+ ) -> Dict[str, Any]:
244
+ """
245
+ Update dashboard listings marked "w/o case" to the target price.
246
+
247
+ The active Playwright page is expected to already be authenticated and on
248
+ the modify listings dashboard.
249
+ """
250
+ output_path = Path(csv_path)
251
+ _ensure_csv(output_path)
252
+
253
+ total_matched = 0
254
+ total_updated = 0
255
+ total_failed = 0
256
+ current_page_number = 1
257
+ current_dashboard_url = page.url or dashboard_url
258
+
259
+ while current_page_number <= max_pages:
260
+ if stop_event and stop_event.is_set():
261
+ logger.info("Listing price update stopped by user")
262
+ break
263
+
264
+ logger.info("Scanning modify listings dashboard page %s", current_page_number)
265
+ _wait_for_dashboard(page)
266
+
267
+ if status_callback:
268
+ status_callback({"state": "Scanning dashboard", "current_page": current_page_number})
269
+
270
+ listings = _scan_current_dashboard_page(page)
271
+ total_matched += len(listings)
272
+
273
+ for listing in listings:
274
+ if stop_event and stop_event.is_set():
275
+ logger.info("Listing price update stopped by user")
276
+ break
277
+
278
+ try:
279
+ _process_listing(page, listing, target_price)
280
+ _append_change(output_path, listing, target_price)
281
+ total_updated += 1
282
+ logger.info("Updated listing %s to %s", listing["listing_id"], target_price)
283
+ except Exception as exc:
284
+ total_failed += 1
285
+ logger.exception("Failed to update listing %s: %s", listing.get("listing_id"), exc)
286
+ finally:
287
+ page.goto(current_dashboard_url, wait_until="domcontentloaded", timeout=30000)
288
+ _wait_after_click(page)
289
+
290
+ if status_callback:
291
+ status_callback(
292
+ {
293
+ "state": "Updating prices",
294
+ "current_page": current_page_number,
295
+ "matched": total_matched,
296
+ "updated": total_updated,
297
+ "failed": total_failed,
298
+ }
299
+ )
300
+
301
+ if stop_event and stop_event.is_set():
302
+ break
303
+
304
+ logger.info("Finished dashboard page %s; checking pagination", current_page_number)
305
+ if not _click_next_page(page):
306
+ logger.info("No enabled next page control found. Listing price update complete.")
307
+ break
308
+
309
+ current_page_number += 1
310
+ current_dashboard_url = page.url or current_dashboard_url
311
+
312
+ return {
313
+ "matched": total_matched,
314
+ "updated": total_updated,
315
+ "failed": total_failed,
316
+ "pages_scanned": current_page_number,
317
+ "csv_path": str(output_path.resolve()),
318
+ }
app/services/run_bot.py CHANGED
@@ -1,6 +1,5 @@
1
  import logging
2
  import threading
3
- import time
4
  from dataclasses import dataclass, field
5
  from datetime import datetime
6
  from pathlib import Path
@@ -9,6 +8,7 @@ from typing import Any, Dict, List, Optional
9
  from auth_handler import AuthHandler
10
  from browser_session import BrowserSession
11
  from config import HARDCODED_USERNAME, HARDCODED_PASSWORD
 
12
 
13
  logger = logging.getLogger("bot")
14
 
@@ -81,8 +81,11 @@ class AutomationController:
81
  elif "opened" in lowered:
82
  self._state.current_state = "Page loaded"
83
  self._state.progress = max(self._state.progress, 50)
84
- elif "browser active" in lowered:
85
- self._state.current_state = "Browser active"
 
 
 
86
  self._state.progress = max(self._state.progress, 100)
87
 
88
  def _set_state(self, **updates: Any) -> None:
@@ -168,13 +171,26 @@ class AutomationController:
168
  self.append_log(f"Opened: {target_url}")
169
  self._set_state(progress=50, current_state="Page loaded")
170
 
171
- self.append_log("Phase 3 ▶ Browser active — click Stop to close")
172
- self._set_state(progress=100, current_state="Browser active")
173
- while True:
174
- if self._stop_event.is_set():
175
- self.append_log("Stopped by user")
176
- break
177
- time.sleep(0.5)
 
 
 
 
 
 
 
 
 
 
 
 
 
178
 
179
  except Exception as exc:
180
  logger.exception("Unexpected error in web runner")
 
1
  import logging
2
  import threading
 
3
  from dataclasses import dataclass, field
4
  from datetime import datetime
5
  from pathlib import Path
 
8
  from auth_handler import AuthHandler
9
  from browser_session import BrowserSession
10
  from config import HARDCODED_USERNAME, HARDCODED_PASSWORD
11
+ from modify_listings_updater import update_wo_case_listing_prices
12
 
13
  logger = logging.getLogger("bot")
14
 
 
81
  elif "opened" in lowered:
82
  self._state.current_state = "Page loaded"
83
  self._state.progress = max(self._state.progress, 50)
84
+ elif "updating w/o case" in lowered:
85
+ self._state.current_state = "Updating prices"
86
+ self._state.progress = max(self._state.progress, 60)
87
+ elif "update complete" in lowered:
88
+ self._state.current_state = "Complete"
89
  self._state.progress = max(self._state.progress, 100)
90
 
91
  def _set_state(self, **updates: Any) -> None:
 
171
  self.append_log(f"Opened: {target_url}")
172
  self._set_state(progress=50, current_state="Page loaded")
173
 
174
+ self._set_state(progress=60, current_state="Updating prices")
175
+ self.append_log("Phase 3 - Updating w/o case listing prices")
176
+ result = update_wo_case_listing_prices(
177
+ session.page,
178
+ dashboard_url=target_url,
179
+ stop_event=self._stop_event,
180
+ )
181
+ self.append_log(
182
+ "Update complete: "
183
+ f"matched={result['matched']}, updated={result['updated']}, "
184
+ f"failed={result['failed']}, pages={result['pages_scanned']}"
185
+ )
186
+ self.append_log(f"CSV saved: {result['csv_path']}")
187
+ self._set_state(
188
+ progress=100,
189
+ current_state="Complete",
190
+ total_items_found=result["matched"],
191
+ current_page=result["pages_scanned"],
192
+ last_csv_path=result["csv_path"],
193
+ )
194
 
195
  except Exception as exc:
196
  logger.exception("Unexpected error in web runner")