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 ← зібрані чекіни (рейтинг, коментар, флейвори)
beer_{id}.jsontasks/{tid}_proc.json. При наступному запуску всі URL здадуться «новими».
[Сторінка пива на 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-ключем). Дублювань немає.
Задача може містити будь-яку кількість URL пива. Логіка роботи відрізняється:
beer_{id}.json файлах| Кнопка | Що створюється | Коли використовувати |
|---|---|---|
+ ОДИН ВІДЖЕТ |
1 задача з усіма обраними URL | Хочете одне вікно на всі обрані сорти, агрегована статистика |
+ ПО ПИВОВАРНЯМ |
N задач, по одній на пивоварню (видно якщо обрано >1 пивоварню) | Зручно обробити кілька пивоварень — кожна в окремому вікні з аналітикою |
+ ОКРЕМІ |
M задач, кожна з одним URL | Коли потрібно індивідуально моніторити кожен сорт окремо |
beer_{id}.json, а не дублюються. Але proc_set — окремий для кожної задачі, тому одна задача може «не знати», що цей URL вже оброблено іншою.
Пріоритет — це вага в черзі колектора, а не в fetcher'і. Важливо розуміти, що саме він впливає на:
Колектор (_collector_loop) перебирає активні задачі в порядку як вони з'явились в store.all() — тобто за часом створення, не за пріоритетом. Пріоритет наразі впливає тільки на позицію в fetcher черзі через Round-Robin.
active_tasks у fekcher'і за priority descending). На практиці різниця помітна лише при 3+ задачах одночасно.
| Значення | Поведінка |
|---|---|
10 | Максимальний пріоритет. При RR — обробляється першим серед усіх |
5 (default) | Стандарт. Рівноправна черга |
1 | Фоновий збір. Обробляється після решти |
Рекомендація: використовуйте пріоритет 8–10 для свіжих важливих пивоварень, 5 для поточної бази, 1–2 для архівних або давно зупинених.
Deep Scroll — режим, при якому колектор не зупиняється автоматично після того, як знаходить підряд 10 вже відомих чекінів.
Прокрутка сторінки вниз (до coll_max_clicks разів)
↓ перевіряє останні 10 чекінів
→ якщо всі 10 вже в proc_set → СТОП (вважає, що решта — теж відомі)
Прокрутка сторінки вниз (до coll_max_clicks разів)
↓ ігнорує перевірку "10 відомих підряд"
→ завжди прокручує до максимальної глибини (coll_max_clicks)
Пресет — множник затримок між запитами. Є глобальний (Налаштування → Обробник) і поперетинковий (у кожному віджеті окремо).
| Пресет | Множник | Затримка при min=1.0, max=2.0 | Ризик |
|---|---|---|---|
Агресивний | ×0.4 | 0.4–0.8 с | Високий. Можливий бан |
Стандартний | ×1.0 | 1.0–2.0 с | Помірний |
Обережний | ×2.0 | 2.0–4.0 с | Низький |
Системна | — | Береться з глобального налаштування |
Задача має пресет "Системна"?
↓ ТАК → використовується глобальний пресет з Налаштувань
↓ НІ → використовується пресет задачі (ігнорує глобальний)
| Параметр | Що регулює | Пресет впливає? |
|---|---|---|
coll_pause_min/max | Пауза між прокрутками сторінки пива (2–4 с за замовч.) | ТАК |
coll_max_clicks | Макс. кількість прокруток (кнопок "Показати ще") | НІ |
coll_timeout_ms | Таймаут завантаження сторінки пива | НІ |
| Параметр | Що регулює | Пресет впливає? |
|---|---|---|
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_burst_pause_m в UI підписано як "хвилини" (на слайдері), але внутрішньо зберігається як число і одразу множиться на 60 для отримання секунд: total_secs = int(pm × 60). Якщо ви бачите значення 5 — це 5 ХВИЛИН (300 секунд).
0 = паузи між чекінами не пишуться в лог (лише тимчасовий індикатор)3.0 — будь-яка пауза ≥ 3 с запишеться у постійний лог як ⏸ Пауза: 3.7сFetcher пускає всі активні задачі через один asyncio.Semaphore(parallel_tabs). Один слот = одна корутина _fetch_burst(tid), яка послідовно відкриває вкладки для свого батчу.
parallel_tabs=2 означає не 2 вкладки одночасно, а 2 батчі (дві різні задачі) паралельно. Всередині одного батчу чекіни обробляються послідовно.
| parallel_tabs | Активних задач | Поведінка |
|---|---|---|
1 | 1 | A обробляє батч, семафор повністю зайнятий |
1 | 3 | A → потім B → потім C (строго по черзі, без паралелізму) |
2 | 3 | A і B паралельно; C чекає поки один звільнить слот |
2 | 1 | A бере 1 слот із 2. Другий слот пустує — марне витрачання налаштування |
3 | 3 | A, B, C — всі три одночасно, без черги |
Поки задача чекає семафора — у її терміналі з'являється: «Очікую черги Round-Robin... 00хв 03с»
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() ← звільняє тільки тут
| parallel_tabs | Задач | Під час burst-паузи A | IP-трафік | Стелс-ефект |
|---|---|---|---|---|
1 | 1 | Єдиний слот зайнятий A (пауза). Нікого більше немає | Зупиняється повністю | ✅ Максимальний |
1 | 3 | Єдиний слот заблоковано A-паузою. B і C стоять у черзі | Зупиняється повністю | ✅ Максимальний |
2 | 3 | A тримає слот 1 (пауза). B обробляє на слоті 2. C чекає | Продовжується (через B) | ❌ Відсутній |
3 | 3 | A тримає слот 1 (пауза). B на слоті 2, C на слоті 3 | Продовжується (через B, C) | ❌ Відсутній |
parallel_tabs=1. При більшому паралелізмі IP продовжує генерувати трафік через інші задачі — пауза однієї нічого не дає. Тому в UI налаштування Burst Control переміщено під параметр parallel_tabs і автоматично вимикається при >1 вкладці.
| Мета | parallel_tabs | burst_size | bot_preset |
|---|---|---|---|
| Максимальний стелс (1 задача) | 1 | 30–50 | Обережний |
| Максимальний стелс (кілька задач) | 1 | 0 | Стандартний |
| Баланс швидкості та стелсу | 2 | 0 | Стандартний |
| Максимальна швидкість (ризик) | 3 | 0 | Агресивний |
self._burst_counters[tid]| type | Значення / дані |
|---|---|
task_update | Новий стан задачі (статус, прогрес, stats) |
log | Рядок логу. [~~] — тимчасовий (зникає з наступним логом) |
engine | Стан рушія: running, current_task |
task_deleted | Задача видалена → видалити віджет |
auth_lost | Витікла сесія Untappd → банер |
library_update | Оновити кеш пивотеки |
[~~]-логи: не зберігаються у {tid}_log.txt, не з'являються в глобальному журналі. Замінюють попередній тимчасовий рядок у терміналі.
| Симптом | Рішення |
|---|---|
| Задача "зависла" у 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 різних пивоварень. |
Рушій — це один процес з двома незалежними паралельними корутинами. Кожна може бути вимкнена окремо.
┌──────────────────────────────────────────────────────────┐ │ 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-паузи (хвилин) |
Задачі: [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с]
parallel_tabs > 1 — задачі B і C продовжують роботу. Загальний трафік не зменшується. Burst ефективний як stealth лише при parallel_tabs = 1.
| Колектор | Обробник | |
|---|---|---|
| Мета | Знайти адреси чекінів | Завантажити вміст чекінів |
| Вкладки | 1 (завжди) | 1–5 (налаштовується) |
| Паралелізм | ❌ Послідовно | ✅ Паралельно між задачами |
| Round-Robin | ❌ Відсутній | ✅ Є |
| Стопить якщо | Немає задач у черзі | Немає pending_urls |
Кожен віджет має статус, який відображається кольоровим бейджем у лівому нижньому куті. Ось повна таблиця з умовами переходу:
| Статус | Колір | Що означає | Умова появи |
|---|---|---|---|
| IDLE | ■ сірий | Задача створена, але не поставлена в чергу. Колектор і обробник її ігнорують. | Задача щойно створена або зупинена через cancel_task() |
| QUEUED | ■ жовтий | Задача чекає обробки колектором. Вона в черзі, але колектор ще не дійшов до неї (або обробляє іншу задачу). | Натиснута ▶ (queue_task). Статус = queued якщо pending_urls порожні. Після запуску колектора → одразу переходить в RUNNING. |
| RUNNING | ■ зелений | Колектор активно збирає URL чекінів АБО обробник обробляє чекіни (або обидва одночасно). Активний стан. |
|
| PAUSED | ■ синій | Задача тимчасово зупинена. Колектор і обробник її пропускають. pending_urls збережені — при відновленні продовжить з тієї ж точки. |
|
| 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. Будується з кількох компонентів:
В черзі: 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 був недавно | Все нормально. Обробник працює. |
+45 · Всього: 1247 · Наступний: 2:34:17
| Елемент | Значення |
|---|---|
+N | Скільки нових чекінів додано в останньому запуску |
Всього: M | Сума всіх чекінів за всі запуски задачі |
Наступний: H:MM:SS | Зворотний відлік до автозапуску (якщо увімкнено розклад) |
IDLE: "3 URLs" ← скільки URL в задачі PAUSED: "PAUSED" ← просто текст статусу
Колектор обходить сторінку пива зверху вниз (від нових до старих — 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" при аналізі |