🛠 Довідка Модератора v2.6

1. Архітектура файлів

Untappd_flavors/
├── tasks/
│   ├── tasks.json              ← стан ВСІХ задач (статус, pending_urls, прогрес)
│   ├── {tid}_proc.json         ← "оброблено назавжди" — Set всіх готових URL
│   ├── {tid}_log.txt           ← текстовий лог виконання
│   └── engine_settings.json    ← глобальні налаштування рушія
│
└── beer_data/
    ├── beer_index.json         ← індекс: URL пива → файл + назва + пивоварня
    └── untappd/
        └── beer_{id}.json      ← зібрані чекіни (рейтинг, коментар, флейвори)
    

2. Відстеження оброблених URL

📁 {tid}_proc.json — "Оброблено назавжди"

  • Set всіх URL чекінів, що вже збережені в beer_{id}.json
  • Читається при кожному зборі → колектор ігнорує вже відомі
  • Оновлюється після кожного батчу
  • Ніколи не очищається автоматично

📋 pending_urls в tasks.json — "Черга"

  • URL зібрані але ще не оброблені fetcher'ом
  • Зберігається в tasks.json → переживає перезапуск
  • Fetcher бере звідси батчами і видаляє опрацьовані
Щоб примусово перезібрати всі чекіни: видаліть tasks/{tid}_proc.json. При наступному запуску всі URL здадуться «новими».

3. Потік даних: від сторінки до файлу

  [Сторінка пива на Untappd]
          │
          ▼  Collector (щогодини або при запуску)
  [Скрапінг посилань на чекіни зі сторінки пива]
          │
          ▼  Фільтр: url NOT IN proc_set AND url NOT IN pending_urls
  [Нові URL → t.pending_urls → tasks.json]
          │
          ▼  Fetcher (постійно, пакетами)
  [Відкриває кожну сторінку чекіна окремо]
          │
          ▼  JS evaluate: user, rating, comment, flavors, date
  [beer_data/untappd/beer_{id}.json] ← злиття за ключем URL
          │
          ▼  URL → proc_set → {tid}_proc.json
    

Злиття в beer_{id}.json: нові чекіни додаються, вже відомі — оновлюються (перезаписуються за URL-ключем). Дублювань немає.

4. Один URL vs Кілька URL у задачі

Задача може містити будь-яку кількість URL пива. Логіка роботи відрізняється:

🍺 Один URL (однолінковий віджет)

  • При зборі: колектор відвідує лише одну сторінку пива
  • Вся статистика (рейтинги, флейвори, графіки) відноситься до одного сорту
  • Прогрес-бар: кількість зібраних чекінів саме цього пива / загальна кількість
  • Вкладка «Результати» показує чисту статистику одного сорту
  • Рекомендується для детального моніторингу важливого сорту

🍻 Кілька URL (груповий віджет)

  • Колектор послідовно обходить кожен URL в задачі
  • Всі чекіни зберігаються в окремих beer_{id}.json файлах
  • Статистика на вкладці «Результати» — агрегована по всіх URL задачі
  • Прогрес-бар = загальна кількість оброблених чекінів / сума по всіх URL
  • Типово: вся пивоварня в одному віджеті

Порівняння режимів створення з бібліотеки

КнопкаЩо створюєтьсяКоли використовувати
+ ОДИН ВІДЖЕТ 1 задача з усіма обраними URL Хочете одне вікно на всі обрані сорти, агрегована статистика
+ ПО ПИВОВАРНЯМ N задач, по одній на пивоварню (видно якщо обрано >1 пивоварню) Зручно обробити кілька пивоварень — кожна в окремому вікні з аналітикою
+ ОКРЕМІ M задач, кожна з одним URL Коли потрібно індивідуально моніторити кожен сорт окремо
⚠ Важливо: Один і той самий URL може бути у різних задачах одночасно. Дані зберігаються в одному beer_{id}.json, а не дублюються. Але proc_set — окремий для кожної задачі, тому одна задача може «не знати», що цей URL вже оброблено іншою.

5. Пріоритети (1–10)

Пріоритет — це вага в черзі колектора, а не в fetcher'і. Важливо розуміти, що саме він впливає на:

Як реально працює пріоритет:

Колектор (_collector_loop) перебирає активні задачі в порядку як вони з'явились в store.all() — тобто за часом створення, не за пріоритетом. Пріоритет наразі впливає тільки на позицію в fetcher черзі через Round-Robin.

Фактичний ефект пріоритету: завдання з вищим пріоритетом отримує більше батчів в Round-Robin циклі відносно інших (через сортування active_tasks у fekcher'і за priority descending). На практиці різниця помітна лише при 3+ задачах одночасно.
ЗначенняПоведінка
10Максимальний пріоритет. При RR — обробляється першим серед усіх
5 (default)Стандарт. Рівноправна черга
1Фоновий збір. Обробляється після решти

Рекомендація: використовуйте пріоритет 8–10 для свіжих важливих пивоварень, 5 для поточної бази, 1–2 для архівних або давно зупинених.

6. Deep Scroll

Deep Scroll — режим, при якому колектор не зупиняється автоматично після того, як знаходить підряд 10 вже відомих чекінів.

Стандартний режим (Deep Scroll = OFF):

  Прокрутка сторінки вниз (до coll_max_clicks разів)
      ↓ перевіряє останні 10 чекінів
      → якщо всі 10 вже в proc_set → СТОП (вважає, що решта — теж відомі)
    

Deep Scroll = ON:

  Прокрутка сторінки вниз (до coll_max_clicks разів)
      ↓ ігнорує перевірку "10 відомих підряд"
      → завжди прокручує до максимальної глибини (coll_max_clicks)
    

✅ Коли вмикати

  • Пиво з нерівномірними чекінами (перерви в активності)
  • Першочерговий збір нового сорту з великою історією
  • Підозрюєте, що деякі чекіни були «пропущені» в середині списку

❌ Коли вимикати

  • Регулярний моніторинг (тільки свіжі) — Deep Scroll зупиниться сам
  • Велика пивоварня (10000+ чекінів) — прокрутка займе надто багато часу
  • Якщо потрібна швидкість, а не повнота
coll_max_clicks (Налаштування → Збирач) — максимальна кількість натискань «Показати ще» на сторінці пива, незалежно від Deep Scroll. Кожен клік = ~25 нових чекінів у списку.

7. Поведінка бота (Bot Preset)

Пресет — множник затримок між запитами. Є глобальний (Налаштування → Обробник) і поперетинковий (у кожному віджеті окремо).

Як множник застосовується:

delay = random(fetch_pause_min, fetch_pause_max) × mult
ПресетМножникЗатримка при min=1.0, max=2.0Ризик
Агресивний×0.40.4–0.8 сВисокий. Можливий бан
Стандартний×1.01.0–2.0 сПомірний
Обережний×2.02.0–4.0 сНизький
СистемнаБереться з глобального налаштування

Ієрархія пресетів:

  Задача має пресет "Системна"?
      ↓ ТАК → використовується глобальний пресет з Налаштувань
      ↓ НІ  → використовується пресет задачі (ігнорує глобальний)
    
Сценарій: Глобально — «Стандартний». Для однієї чутливої пивоварні встановіть «Обережний» на рівні задачі. Решта задач залишаться зі стандартними затримками.

8. Таймінги, паузи та їх взаємодія з пресетами

Параметри колектора (Налаштування → Збирач):

ПараметрЩо регулюєПресет впливає?
coll_pause_min/maxПауза між прокрутками сторінки пива (2–4 с за замовч.)ТАК
coll_max_clicksМакс. кількість прокруток (кнопок "Показати ще")НІ
coll_timeout_msТаймаут завантаження сторінки пиваНІ

Параметри fetcher'а (Налаштування → Обробник):

ПараметрЩо регулюєПресет впливає?
fetch_pause_min/maxПауза після завантаження сторінки чекінаТАК
fetch_timeout_msТаймаут завантаження сторінки чекінаНІ
fetch_burst_sizeКількість чекінів перед burst-паузою (0=вимк.)НІ
fetch_burst_pause_mТривалість burst-паузи в секундахНІ
batch_sizeКількість чекінів за один батч у Round-RobinНІ
parallel_tabsКількість одночасних вкладок браузераНІ

Повна схема затримок за один цикл чекіна:

  1. Відкрити нову вкладку браузера
  2. goto(url)  ← показує індикатор "Завантаження... Xс"
  3. fetch_pause_min × mult ... fetch_pause_max × mult  ← рандомна пауза
         (якщо затримка ≥ 0.5с → показується тимчасовий індикатор; якщо ≥ log_threshold → записується в лог)
  4. JS evaluate → зібрати дані
  5. Закрити вкладку
  6. [якщо burst_count % burst_size == 0] → burst_pause_m секунд
         (індикатор "Burst-пауза: XXс..." щосекунди)
    

Приклад: "Обережний" з довгими паузами

fetch_pause = random(1.0, 2.0) × 2.0 = 2.0–4.0 с між кожним чекіном
+ burst кожні 20 чекінів на 300 с (5 хвилин)
Підводний камінь: fetch_burst_pause_m в UI підписано як "хвилини" (на слайдері), але внутрішньо зберігається як число і одразу множиться на 60 для отримання секунд: total_secs = int(pm × 60). Якщо ви бачите значення 5 — це 5 ХВИЛИН (300 секунд).

Налаштування "Логувати паузи довше N сек" (окремо для кожної задачі)

9. Round-Robin, parallel_tabs та Burst-пауза

Що таке "слот" і як семафор ділить роботу

Fetcher пускає всі активні задачі через один asyncio.Semaphore(parallel_tabs). Один слот = одна корутина _fetch_burst(tid), яка послідовно відкриває вкладки для свого батчу.

Важливо: parallel_tabs=2 означає не 2 вкладки одночасно, а 2 батчі (дві різні задачі) паралельно. Всередині одного батчу чекіни обробляються послідовно.

Алгоритм для різних конфігурацій

parallel_tabsАктивних задачПоведінка
11A обробляє батч, семафор повністю зайнятий
13A → потім B → потім C (строго по черзі, без паралелізму)
23A і B паралельно; C чекає поки один звільнить слот
21A бере 1 слот із 2. Другий слот пустує — марне витрачання налаштування
33A, B, C — всі три одночасно, без черги

Поки задача чекає семафора — у її терміналі з'являється: «Очікую черги Round-Robin... 00хв 03с»

Burst-пауза та її взаємодія з parallel_tabs

Burst-пауза відбувається всередині _fetch_burst(tid) — поки задача спить, вона продовжує тримати свій слот семафора.

  _fetch_burst(A, sem):
      acquire(sem)  ← захоплює слот
      try:
          for cu in batch:
              ...обробка чекінів...
              якщо burst_count % burst_size == 0:
                  sleep(300с)  ← ПАУЗА ТУТ, слот ВСЕ ЩЕ УТРИМУЄТЬСЯ
      finally:
          sem.release()  ← звільняє тільки тут

Матриця: що реально відбувається під час burst-паузи задачі A

parallel_tabsЗадачПід час burst-паузи AIP-трафікСтелс-ефект
11 Єдиний слот зайнятий A (пауза). Нікого більше немає Зупиняється повністю ✅ Максимальний
13 Єдиний слот заблоковано A-паузою. B і C стоять у черзі Зупиняється повністю ✅ Максимальний
23 A тримає слот 1 (пауза). B обробляє на слоті 2. C чекає Продовжується (через B) ❌ Відсутній
33 A тримає слот 1 (пауза). B на слоті 2, C на слоті 3 Продовжується (через B, C) ❌ Відсутній
Висновок: Burst-пауза має реальний стелс-ефект тільки при parallel_tabs=1. При більшому паралелізмі IP продовжує генерувати трафік через інші задачі — пауза однієї нічого не дає. Тому в UI налаштування Burst Control переміщено під параметр parallel_tabs і автоматично вимикається при >1 вкладці.

Практичні рекомендації

Метаparallel_tabsburst_sizebot_preset
Максимальний стелс (1 задача)130–50Обережний
Максимальний стелс (кілька задач)10Стандартний
Баланс швидкості та стелсу20Стандартний
Максимальна швидкість (ризик)30Агресивний

Burst-лічильники — деталі

10. WebSocket та REST API

typeЗначення / дані
task_updateНовий стан задачі (статус, прогрес, stats)
logРядок логу. [~~] — тимчасовий (зникає з наступним логом)
engineСтан рушія: running, current_task
task_deletedЗадача видалена → видалити віджет
auth_lostВитікла сесія Untappd → банер
library_updateОновити кеш пивотеки

[~~]-логи: не зберігаються у {tid}_log.txt, не з'являються в глобальному журналі. Замінюють попередній тимчасовий рядок у терміналі.

11. Усунення несправностей

СимптомРішення
Задача "зависла" у running після перезапускуПри старті всі running → paused автоматично. Натисніть ▶.
Потрібно зібрати всі чекіни зановоВидаліть tasks/{tid}_proc.json
Burst-пауза не спрацьовуєПереконайтесь: fetch_burst_size > 0. Лічильник накопичується — при batch_size=5 і burst_size=20 пауза настане після 4 батчів.
Браузер не запускається (GPU crash)Видаліть playwright_profile/ShaderCache через Налаштування → Профіль браузера
Сесія закінчиласьЧервоний банер з'явиться автоматично. Залогіньтесь через "Ручний логін" або відкрийте Chrome без headless.
Пауза між чекінами не видна в лозіВстановіть "Логувати паузи довше N сек" у налаштуваннях задачі (вкладка ⚙ у віджеті).
ПО ПИВОВАРНЯМ не з'являється в бібліотеціПотрібно вибрати >1 сорту з >1 різних пивоварень.

12. Колектор і Обробник — механіка роботи

Рушій — це один процес з двома незалежними паралельними корутинами. Кожна може бути вимкнена окремо.

┌──────────────────────────────────────────────────────────┐
│                     ENGINE PROCESS                       │
│                                                          │
│  _collector_loop()            _fetcher_loop()            │
│  ═════════════════            ══════════════             │
│  1 вкладка, послідовно        N вкладок, паралельно      │
│                                                          │
│  beer URL → checkin URLs      checkin URL → смаки/оцінка │
└──────────────────────────────────────────────────────────┘

Колектор

Відвідує сторінку пива (/b/brewery-beer/ID) і збирає URL чекінів кліками "Load More".

ПараметрЩо означає
1 вкладкаЗавжди одна сторінка — жодного паралелізму між задачами
ПослідовноЗадача А повністю, потім задача Б — без чергування
coll_pause_min/maxПауза між кліками "Завантажити ще"
coll_max_clicksМаксимум натискань "Завантажити ще" на сторінці
coll_timeout_msТаймаут page.goto() (за замовч. 60 000 мс)
task.urls = ["https://untappd.com/b/varvar-brew-black-bean/3548624"]
                        ↓  колектор
task.pending_urls = [checkin_1, checkin_2, ... checkin_269]  ← черга для обробника

Обробник

Відвідує кожну сторінку чекіна (/c/user/id) і зберігає: смаки, оцінку, дату, фото.

ПараметрЩо означає
Round-RobinЗадачі чергуються: A → B → C → A → B → C
Паралельні вкладкиОдночасно parallel_tabs вкладок в браузері
batch_sizeСкільки URL обробляється за один "виток" Round-Robin
fetch_pause_min/maxПауза між окремими чекінами всередині батчу
fetch_burst_sizeПісля N чекінів — довга burst-пауза
fetch_burst_pause_mТривалість burst-паузи (хвилин)

Round-Robin детально

Задачі: [A, B, C]    parallel_tabs=3    batch_size=5

Цикл 1 → gather одночасно:
  A: обробляє checkins 1–5   [вкладка 1]
  B: обробляє checkins 1–5   [вкладка 2]
  C: обробляє checkins 1–5   [вкладка 3]

Цикл 2 (зміщення на 1) → gather одночасно:
  B: обробляє checkins 6–10  [вкладка 1]
  C: обробляє checkins 6–10  [вкладка 2]
  A: обробляє checkins 6–10  [вкладка 3]

► Задача з 1000 чекінів не блокує задачу з 50.
► Кожна задача просувається рівномірно.

Семафор і вкладки

parallel_tabs = 3  →  Semaphore(3)

  [вкладка 1] - A batch  ┐
  [вкладка 2] - B batch  ├─ одночасно
  [вкладка 3] - C batch  ┘

Якщо parallel_tabs = 1:
  [вкладка 1] - A batch → чекає → B batch → чекає → C batch   (послідовно)

Поки всі слоти зайняті, задача показує: [~~] Очікую черги Round-Robin... 00хв 12с

Паузи — де і коли

КОЛЕКТОР:
  page.goto()           → [coll_timeout_ms, макс 60с]
  перед кліком          → [coll_pause_min × 0.5 .. coll_pause_min]
  після кожного кліку   → [coll_pause_min .. coll_pause_max]

ОБРОБНИК:
  між кожним чекіном    → [fetch_pause_min .. fetch_pause_max]
  кожні burst_size чек. → [burst_pause_m × 60 секунд]
  page.goto()           → [fetch_timeout_ms, макс 30с]
⚠ Burst-пауза і кілька вкладок: Burst-пауза зупиняє конкретну задачу А, але якщо parallel_tabs > 1 — задачі B і C продовжують роботу. Загальний трафік не зменшується. Burst ефективний як stealth лише при parallel_tabs = 1.

Ключова різниця

КолекторОбробник
МетаЗнайти адреси чекінівЗавантажити вміст чекінів
Вкладки1 (завжди)1–5 (налаштовується)
Паралелізм❌ Послідовно✅ Паралельно між задачами
Round-Robin❌ Відсутній✅ Є
Стопить якщоНемає задач у черзіНемає pending_urls

13. Статуси віджетів

Кожен віджет має статус, який відображається кольоровим бейджем у лівому нижньому куті. Ось повна таблиця з умовами переходу:

СтатусКолірЩо означаєУмова появи
IDLE ■ сірий Задача створена, але не поставлена в чергу. Колектор і обробник її ігнорують. Задача щойно створена або зупинена через cancel_task()
QUEUED ■ жовтий Задача чекає обробки колектором. Вона в черзі, але колектор ще не дійшов до неї (або обробляє іншу задачу). Натиснута ▶ (queue_task). Статус = queued якщо pending_urls порожні. Після запуску колектора → одразу переходить в RUNNING.
RUNNING ■ зелений Колектор активно збирає URL чекінів АБО обробник обробляє чекіни (або обидва одночасно). Активний стан.
  • Колектор взяв задачу зі статусу queued → ставить running
  • АБО: pending_urls вже були при старті → одразу running
PAUSED ■ синій Задача тимчасово зупинена. Колектор і обробник її пропускають. pending_urls збережені — при відновленні продовжить з тієї ж точки.
  • Натиснута ⏸ (pause_task)
  • Виявлена втрата авторизації Untappd (_auth_lost)
  • При перезапуску сервера: всі runningpaused автоматично
DONE ■ фіолетовий Збір завершено (collect_done = True) і черга порожня (pending_urls = []). Всі чекіни записані у файл. Обробник виявив що pending_urls пусті І collect_done = True.
Якщо увімкнено розклад → автоматично переходить в QUEUED через заданий інтервал.
ERROR ■ червоний Критична помилка під час роботи (зарезервовано). В поточній версії більшість помилок логуються але не переводять в error. Зарезервовано для майбутнього використання. Зараз не використовується активно.

Діаграма переходів статусів

              ┌──────────────[Старт сервера: running→]──────────────┐
              ↓                                                      │
    [IDLE] ──▶──── ▶ натиснуто ────→ [QUEUED] ──────────────────→ [PAUSED]
      ↑                                   │                          │
      │ ✕ cancel                          │ Колектор взяв задачу     │ ▶ відновлено
      │                                   ↓                          │
    [DONE] ←────────────────────── [RUNNING] ←───────────────────────┘
      │         pending=[] AND             │
      │         collect_done=True          │ ⏸ пауза / auth lost
      │                                    ↓
      └─── [розклад увімкнено] ─────→ [QUEUED]        [PAUSED]

Статус-бар у нижній частині віджета (RUNNING)

З'являється коли задача RUNNING. Будується з кількох компонентів:

В черзі: 177 · Оброблено: 4 · 1.2/хв · ~145хв · збір ✓ ⚠️ 21хв без активності
─────────────────────────────────────────────────────────────────
    [1]           [2]        [3]         [4]      [5]       [6]
#ЕлементЗначенняКоли присутній
[1] В черзі: N Кількість URL чекінів у pending_urls — ще не оброблені обробником Завжди (коли RUNNING)
[2] Оброблено: M Кількість чекінів успішно сфетчених в поточному запуску задачі. Скидається при новому запуску. Завжди (коли RUNNING)
[3] N/хв зеленим Поточна швидкість обробки (чекінів за хвилину). Розраховується на основі progress.speed. Тільки якщо обробник вже починав роботу
[4] ~Nхв Орієнтовний час до завершення (ETA). pending / speed. Змінюється динамічно. Тільки якщо є progress.eta_s
[5] збір ✓ або збір... збір ✓ — колектор завершив збір URL (collect_done = True)
збір... з пульсацією — колектор ще в роботі
Завжди
[6] Одне з попереджень — детально нижче

Попередження у статус-барі

ПопередженняУмова появиЩо означає
⚠️ Nхв без активності pending > 0 І done > 0 І останній fetch більше 2 хвилин тому Обробник зупинився але черга не порожня. Можливо: браузер завис, burst-пауза, або обробник вимкнено після старту.
⊘ обробник вимкнено pending > 0 І обробник вимкнений в налаштуваннях (fetcher_enabled=false) URL зібрані, чекають у черзі — обробник треба увімкнути вручну через Налаштування → Обробник.
⚠️ Обробник не запустився pending > 0 І collect_done = True І last_fetch_at = null І обробник enabled Колектор завершив, URL є, але обробник жодного разу не запускався. Можливо: браузер не ініціалізований або семафор заблокований.
(порожньо) Обробник активний і fetch був недавно Все нормально. Обробник працює.

Статус-бар у стані DONE

+45 · Всього: 1247 · Наступний: 2:34:17
ЕлементЗначення
+NСкільки нових чекінів додано в останньому запуску
Всього: MСума всіх чекінів за всі запуски задачі
Наступний: H:MM:SSЗворотний відлік до автозапуску (якщо увімкнено розклад)

Статус-бар у стані IDLE / PAUSED

IDLE:   "3 URLs"        ← скільки URL в задачі
PAUSED: "PAUSED"        ← просто текст статусу
⚠️ "21хв без активності" — найчастіша причина: після burst-паузи обробника, якщо задача А на паузі але задачі B, C продовжують, fetch_at оновлюється тільки для активних задач. Саме тому задача А може показувати "без активності" навіть якщо рушій працює нормально — обробник просто займатися іншими задачами.

14. Порядок збереження чекінів

Чи зберігаються чекіни в хронологічному порядку?

⚠️ Ні. Чекіни зберігаються в порядку збору, а не в хронологічному порядку дат.

Як формується порядок

Колектор обходить сторінку пива зверху вниз (від нових до старих — Untappd відображає чекіни від найновіших):

Untappd сторінка пива (зверху → вниз):
  [checkin_newest]     ← колектор збирає першим
  [checkin_2nd]
  [checkin_3rd]
  ...
  [checkin_oldest]     ← колектор збирає останнім

Отже pending_urls = список від нових до старих. Обробник обробляє їх у тому ж порядку (FIFO).

Злиття при повторному запуску

При кожному наступному запуску задачі, _save_beer зливає нові чекіни зі старими за ключем URL:

ex = {c["url"]: c for c in existing_checkins}   ← dict від старих
for r in new_results: ex[r["url"]] = r           ← update/overwrite
results = list(ex.values())                       ← результат: dict.values() порядок
⚠️ Важливо: dict.values() в Python 3.7+ зберігає порядок вставки. Тобто:

Практичні наслідки

СитуаціяПорядок у файлі
Перший повний збірВід нових до старих (порядок зі сторінки Untappd)
Повторний збір (нові чекіни з'явились)Нові VID чекіни в кінці (бо нові URL додаються до існуючого dict)
Якщо аналіз залежить від датиСортуйте за полем "created_at" при аналізі
ℹ Для аналізу флейворів — порядок не має значення. Статистика (середній рейтинг, топ флейвори, розподіл оцінок) будується агрегацією всіх чекінів незалежно від порядку.