235 lines
9.4 KiB
Python
235 lines
9.4 KiB
Python
"""
|
||
vprok.ru Windows Playwright Client
|
||
====================================
|
||
Каждые 30 секунд проверяет сервер-реле на наличие задания.
|
||
Если задание есть — открывает vprok.ru и добавляет товары в корзину.
|
||
|
||
Требования:
|
||
pip install playwright requests
|
||
playwright install chromium
|
||
"""
|
||
|
||
import time
|
||
import requests
|
||
from playwright.sync_api import sync_playwright, TimeoutError as PwTimeout
|
||
|
||
# ─── Настройки ───────────────────────────────────────────────────────────────
|
||
SERVER_URL = "http://185.130.212.192:5000"
|
||
API_KEY = "vprok2024secret"
|
||
POLL_INTERVAL = 30 # секунды между проверками
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
HEADERS = {"X-Api-Key": API_KEY, "Content-Type": "application/json"}
|
||
|
||
|
||
def log(msg: str):
|
||
ts = time.strftime("%H:%M:%S")
|
||
print(f"[{ts}] {msg}", flush=True)
|
||
|
||
|
||
def fetch_task():
|
||
"""Забрать задание с сервера. Возвращает dict или None."""
|
||
try:
|
||
r = requests.get(f"{SERVER_URL}/task", headers=HEADERS, timeout=10)
|
||
if r.status_code == 204:
|
||
return None
|
||
if r.status_code == 200:
|
||
return r.json()
|
||
log(f"⚠️ Неожиданный статус от сервера: {r.status_code}")
|
||
return None
|
||
except Exception as e:
|
||
log(f"❌ Ошибка подключения к серверу: {e}")
|
||
return None
|
||
|
||
|
||
def report_done(status: str, message: str):
|
||
"""Отправить результат на сервер."""
|
||
try:
|
||
requests.post(
|
||
f"{SERVER_URL}/task/done",
|
||
headers=HEADERS,
|
||
json={"status": status, "message": message},
|
||
timeout=10
|
||
)
|
||
log(f"✅ Результат отправлен: [{status}] {message}")
|
||
except Exception as e:
|
||
log(f"❌ Не удалось отправить результат: {e}")
|
||
|
||
|
||
def add_item_to_cart(page, query: str, qty: int) -> tuple[bool, str]:
|
||
"""
|
||
Ищет товар на vprok.ru и добавляет qty раз в корзину.
|
||
Возвращает (success, message).
|
||
"""
|
||
log(f"🔍 Ищу: «{query}» (кол-во: {qty})")
|
||
|
||
try:
|
||
# Переходим на главную и кликаем поиск
|
||
page.goto("https://www.vprok.ru/", wait_until="domcontentloaded", timeout=30000)
|
||
page.wait_for_timeout(1500)
|
||
|
||
# Находим поле поиска и вводим запрос
|
||
search_input = page.locator(
|
||
"input[placeholder*='оиск'], input[type='search'], [data-testid='search-input'], .search__input"
|
||
).first
|
||
search_input.click()
|
||
search_input.fill("")
|
||
search_input.type(query, delay=60)
|
||
search_input.press("Enter")
|
||
|
||
log(f" ⏳ Жду результаты поиска...")
|
||
page.wait_for_load_state("domcontentloaded")
|
||
page.wait_for_timeout(2500)
|
||
|
||
# Ищем кнопки добавления — берём первый доступный товар
|
||
# vprok.ru использует разные классы, пробуем несколько вариантов
|
||
add_button_selectors = [
|
||
"button:has-text('Добавить'):not([disabled])",
|
||
"button:has-text('В корзину'):not([disabled])",
|
||
"[class*='add-to-cart']:not([disabled])",
|
||
"[data-testid*='add']:not([disabled])",
|
||
]
|
||
|
||
add_btn = None
|
||
for selector in add_button_selectors:
|
||
btns = page.locator(selector)
|
||
count = btns.count()
|
||
if count > 0:
|
||
# Проверяем что рядом нет текста "Нет в наличии"
|
||
for i in range(min(count, 5)):
|
||
btn = btns.nth(i)
|
||
try:
|
||
# Ищем ближайший контейнер товара
|
||
product_card = btn.locator("xpath=ancestor::*[contains(@class,'product') or contains(@class,'item') or contains(@class,'card')][1]")
|
||
card_text = product_card.inner_text(timeout=1000) if product_card.count() > 0 else ""
|
||
if "нет в наличии" in card_text.lower() or "недоступен" in card_text.lower():
|
||
log(f" ⏭️ Товар #{i+1} недоступен, пропускаю")
|
||
continue
|
||
add_btn = btn
|
||
break
|
||
except Exception:
|
||
add_btn = btn
|
||
break
|
||
if add_btn:
|
||
break
|
||
|
||
if add_btn is None:
|
||
return False, f"Кнопка 'Добавить' не найдена для «{query}»"
|
||
|
||
# Добавляем нужное количество раз
|
||
for n in range(qty):
|
||
try:
|
||
add_btn.scroll_into_view_if_needed()
|
||
add_btn.click(timeout=5000)
|
||
log(f" ➕ Добавлено ({n+1}/{qty})")
|
||
page.wait_for_timeout(800)
|
||
|
||
# После первого клика кнопка может смениться на +/-
|
||
if n < qty - 1:
|
||
plus_btn = page.locator(
|
||
"button:has-text('+'), [aria-label*='увеличить'], [class*='plus']:not([disabled])"
|
||
).first
|
||
if plus_btn.count() > 0 and plus_btn.is_visible():
|
||
add_btn = plus_btn
|
||
except PwTimeout:
|
||
log(f" ⚠️ Таймаут при клике на кнопку (попытка {n+1})")
|
||
|
||
return True, f"«{query}» добавлен(о) ×{qty}"
|
||
|
||
except Exception as e:
|
||
return False, f"Ошибка при обработке «{query}»: {e}"
|
||
|
||
|
||
def process_task(task: dict):
|
||
"""Обрабатывает задание: открывает браузер и добавляет все товары."""
|
||
items = task.get("items", [])
|
||
task_id = task.get("id", "?")
|
||
log(f"📦 Задание #{task_id} получено. Товаров: {len(items)}")
|
||
|
||
results = []
|
||
errors = []
|
||
|
||
with sync_playwright() as pw:
|
||
log("🌐 Запускаю браузер...")
|
||
browser = pw.chromium.launch(
|
||
headless=False,
|
||
args=["--start-maximized"]
|
||
)
|
||
context = browser.new_context(
|
||
viewport=None, # использовать размер окна
|
||
locale="ru-RU",
|
||
timezone_id="Europe/Moscow"
|
||
)
|
||
page = context.new_page()
|
||
|
||
for item in items:
|
||
query = item.get("query", "").strip()
|
||
qty = int(item.get("qty", 1))
|
||
if not query:
|
||
continue
|
||
|
||
success, msg = add_item_to_cart(page, query, qty)
|
||
results.append(msg)
|
||
if not success:
|
||
errors.append(msg)
|
||
log(f" {'✅' if success else '❌'} {msg}")
|
||
time.sleep(1)
|
||
|
||
log("🛒 Все товары обработаны. Перехожу в корзину...")
|
||
try:
|
||
# Пытаемся открыть корзину
|
||
cart_link = page.locator("a[href*='cart'], a[href*='basket'], [data-testid*='cart']").first
|
||
if cart_link.count() > 0:
|
||
cart_link.click()
|
||
page.wait_for_load_state("domcontentloaded")
|
||
else:
|
||
page.goto("https://www.vprok.ru/cart", wait_until="domcontentloaded", timeout=15000)
|
||
except Exception:
|
||
pass
|
||
|
||
log("👁️ Браузер оставлен открытым — проверьте корзину!")
|
||
log(" (закройте браузер вручную когда будете готовы)")
|
||
|
||
# Ждём пока пользователь не закроет браузер
|
||
try:
|
||
page.wait_for_event("close", timeout=0) # бесконечно
|
||
except Exception:
|
||
pass
|
||
|
||
# Отправляем результат
|
||
if errors:
|
||
status_str = "partial"
|
||
message = f"Выполнено с ошибками: {'; '.join(errors)}"
|
||
else:
|
||
status_str = "ok"
|
||
message = f"Все {len(results)} товар(ов) добавлены: {', '.join(results)}"
|
||
|
||
report_done(status_str, message)
|
||
|
||
try:
|
||
browser.close()
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
def main():
|
||
log("🚀 vprok.ru клиент запущен")
|
||
log(f" Сервер: {SERVER_URL}")
|
||
log(f" Интервал проверки: {POLL_INTERVAL} сек")
|
||
log(" Нажмите Ctrl+C для остановки\n")
|
||
|
||
while True:
|
||
task = fetch_task()
|
||
if task:
|
||
process_task(task)
|
||
else:
|
||
log(f"💤 Нет заданий, жду {POLL_INTERVAL} сек...")
|
||
time.sleep(POLL_INTERVAL)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
try:
|
||
main()
|
||
except KeyboardInterrupt:
|
||
log("\n👋 Остановка по запросу пользователя")
|