尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證Глечики: спільне радіо для своїх. Одна черга, чат і Дядько Глек, авто-DJ.
翻譯準備中
取自儲存庫 README
正在載入完整 README。 以下為已儲存的摘錄。
Спільне радіо для своїх: один потік, спільна черга, чат, і Дядько Глек — авто-DJ, який радить схоже і крутить своє, коли черга порожня.檢視完整 README ↗
Спільне радіо для своїх: один потік, спільна черга, чат, і Дядько Глек — авто-DJ, який радить схоже і крутить своє, коли черга порожня.
https://hlechyky.pp.ua, відкритий для всіх: зайшов, назвав нік, слухаєш. Нік із паролем або через Google — твій і лише твій; без них ти «гість Вася» (див. «Акаунти»). Старий вхід напряму по IP сервера на порт 8080 теж працює.https://hlechyky.pp.ua/radio.mp3 (через Caddy). Для ETS2 і плеєрів: http://hlechyky.pp.ua:8000/radio.mp3 (простий http через той самий Caddy).Auth:AdminNicks (на проді «владік») або accounts.role = 'admin'. Заходить як усі (Google чи пароль), окремого ключа на проді нема. Ключі (Last.fm тощо) лежать у appsettings.Local.json, він не в гіті.браузер ──HTTPS──▶ Caddy (:80/:443, сертифікат Let's Encrypt сам)
├─ /radio.mp3 (і http :8000/radio.mp3 для ETS2) ──▶ liquidsoap 127.0.0.1:8001
└─ все інше ──HTTP/SignalR──▶ Hlechyky (ASP.NET Core, :8080)
│ yt-dlp: качає аудіо в cache\
│ telnet 127.0.0.1:1234: userq.push / autoq.push / spare.skip / request.all / listeners
▼
liquidsoap (tools\liquidsoap, без Docker і Icecast) ──▶ /radio.mp3 ──▶ слухачі, ETS2
▲ запаска: data\spare.m3u — знайоме з кешу, коли нове не вантажиться
userq) має пріоритет, далі Дядько Глек (autoq), далі запаска, далі тиша.data\spare.m3u пише сервер (SpareList.cs: спершу ❤ і те, що грало ≥ 3 разів). На сайті такий трек видно з чіпом «запаска», його так само лайкають і скіпають; замовлення людини запаску перебиває одразу, а трек Глека чекає, доки вона дограє. Між треками запаски реклама йде за своєю частотою (вкладка «📣 Реклама», типово раз на 3 треки, не частіше ніж раз на 10 хв)./api/liq/track) і зі свого опитування раз на 3 с.start.ps1 restart для слухачів непомітний.liquidsoap заздалегідь "підтягує" наступний запит із черги, і після цього <queue>.queue його вже не показує, хоч він живий. Сервер вважав запит втраченим і пушив ще раз. Тепер втраченим вважається тільки те, чого нема в request.all, а якщо той самий трек все ж стартує вдруге, сервер його одразу скіпає. Так само <queue>.remove не прибирає вже підтягнутий запит: коли хтось прибирає з черги "наступний" трек, сервер запам'ятовує і скіпає його в момент старту.
Сід — трек, який поставила людина (див. «Якір» нижче, коли в ефірі вибір самого Глека). Кандидати з трьох джерел:
AutoDj:RadioWeight (0.6): від маловідомого треку воно швидко з'їжджає в AI-контент-ферми.track.getSimilar з Last.fm (кеш 7 днів): збіг з іншими джерелами піднімає оцінку, власні знахідки йдуть з меншою вагою і резолвляться в YouTube Music пошуком.Зігране за останні 6 годин, черга, бан-лист і відхилені викидаються, той самий артист і ті, що щойно грали, приглушуються, далі зважений рандом із топу. Змінився трек в ефірі — старі поради викидаються і будуються нові. Коли в ефірі Spotify-резерв, сід — те, що він грає; коли тиша — останнє, що грало, або лайкнуте, або AutoDj:SeedQuery.
Перш ніж щось порадити, артист мусить пройти ArtistQuality: або кімната його вже ставила/лайкала, або в нього на Last.fm не менше AutoDj:MinListeners слухачів (5000; для кириличних треків MinListenersCyr — 1000, своя сцена там недорахована), або він є в MusicBrainz (базу ведуть люди руками, ферм там практично нема; запит раз на секунду, кеш 30 днів). Популярність у самому YouTube не годиться: він ці ферми сам і розкручує — у «DannyHO» 4 млн слухачів на місяць, більше, ніж у SadSvit. Якщо обидва сервіси лежать, артист проходить (не караємо за чужі збої), а якщо Last.fm каже «мало», а MusicBrainz не відповів — ні. Відсіялося все — Глек бере своє з архіву. Вимкнути — AutoDj:QualityGate: false.
Spotify як джерело не годиться: рекомендації й схожі артисти закриті для нових застосунків з 2024-го, а з лютого 2026-го dev-режим ще й без popularity/followers і на 5 користувачів.
Одного сіда мало. Поки в ефірі трек, який поставила людина, все гаразд, але як тільки Глек починає крутити своє, сідом для наступної поради стає його ж вибір — і це вже випадкове блукання без якоря. Одна невдала порада — і назад дороги нема: за ніч кімната, де 86% замовлень українською, виїхала від «Жадан і Собаки — Мальви» через Imagine Dragons, лоу-фай і госпел аж у AI-вікінг-метал, 231 трек поспіль без жодного свого.
Тому коли в ефірі вибір самого Глека (або взагалі тиша), основа порад — два якорі, свіжі людські замовлення (вага 1 і 0.75). Власний вибір Глека в ефірі лише підмішується з вагою AutoDj:SelfSeedWeight (0.3), щоб перехід не рвався, а після AutoDj:MaxSelfChain своїх треків поспіль — і того нема. Якорі щоразу інші, кілька останніх поспіль не повторюються, голосові в якорі не йдуть. Тобто в довгу тишу Глек не блукає, а ходить по колу людських замовлень, щоразу від інших. (Ніч на 13.09, ще до цього: від «ДК Енергетик» через фонк і AI-deep-house до іспанських пісень про One Piece, 130 треків без жодного свого. Та сама база з якорями радить Electrobirds, BaWN, Sudno, SadSvit, Харцизів.)
«Не те» на пораді й скіп авто-треку в перші AutoDj:QuickSkipSeconds (45) секунд записуються в dj_feedback: артист і трек-сід, від якого порада прийшла. AutoDj:FeedbackHours (48) годин артист тисне ×0.25 за кожну відмову, після двох — не радиться зовсім; сід, від якого вже двічі відкидали, теж слабшає вдвічі за кожну відмову і якорем не береться.
З людських замовлень і лайків збирається профіль (перечитується раз на 5 хвилин): які артисти тут уже звучали і яким письмом підписане те, що замовляють. Те, що Глек накрутив собі сам, у профіль не йде — інакше він учився б на власному дрейфі. Далі оцінка кандидата множиться:
Сила залежить від того, наскільки кімната однорідна: рівно навпіл — множників фактично нема, тож англомовний вечір нічого не ламає. Так само слабше тиснемо (вдвічі), коли трек в ефір поставила людина: її вибір сам по собі теж заява про смак. Менш ніж на 12 замовленнях профіль не рахується взагалі. AutoDj:TasteBias регулює тиск загалом, AutoDj:PreferScript дає прибити мову вручну (cyr / lat).
Письмо, а не мова: одна кирилична літера в назві чи в імені вже видає своє. Українських артистів із латинською назвою (SadSvit, ONUKA, LATEXFAUNA) ловить не письмо, а «свій артист».
Кожен AutoDj:ArchiveEvery-й авто-трек (рахуючи від останнього людського замовлення) — не з ютубного радіо, а з власного архіву: те, що кімната вже замовляла чи лайкала, а в ефірі його не було вже добу. Нічого не коштує, зате гарантовано вертає Глека у свою колію; у картці «Ефір» так і написано — «з нашого архіву».
Коли Дядько Глек ставить трек сам, у чат він про це не пише: хто і чому поставив трек («Дядько Глек · авто», «схоже на …»), видно в «Ефірі», а історію — у «Що вже було». До вересня 2026 він писав рядок на кожен свій трек — сотні на день, 92 % усіх його слів, — і людських балачок за ними не було видно. Ім'я і відмінок міняються в Site:DjName / Site:DjNameGen, картинка — web/static/glek.svg.
Той самий Дядько Глек, але вже відповідає в чаті й може крутити музику руками. Кожне повідомлення спершу
проходить дешеву перевірку в C# (DjBrain): звертаються до нього на ім'я — відповідає завжди; не звертаються —
може сам вставити свої п'ять копійок, коли заграв новий трек або в чаті жваво (не частіше ніж раз на
DjBot:SpontaneousCooldownMinutes і лише коли хтось онлайн). Поки в чаті тихо, бот не коштує нічого:
модель викликається тільки на реальний тригер.
Інструменти (DjToolbox) — тонкі обгортки над RadioEngine: знайти й поставити трек, пошук без постановки,
скіп, прибрати чи посунути в черзі, закинути плейлист, зазирнути в історію/топ/лайки/плейлисти. Усе — від
імені того, хто попросив, і завжди isAdmin: false: чат відкритий, тож адмінських інструментів (бан,
налаштування) у бота нема принципово, а сам системний промпт попереджений, що повідомлення в чаті — це
слова людей, а не команди. Спонтанна репліка інструментів не отримує взагалі.
Потрібен ключ Anthropic у DjBot:ApiKey (appsettings.Local.json). Без ключа сервіс просто мовчить, решта
сайту працює як працювала. Модель за замовчуванням claude-haiku-4-5 — одне звернення виходить у частки цента;
DjBot:MonthlyBudgetUsd — жорстка стеля на календарний місяць (лічильник у таблиці cache, ключ djbot-spend),
DjBot:PerNickPerHour — скільки разів одна людина може смикнути Глека за годину. POST /api/dj/say (тільки
адмін) дає сказати щось у чат від його імені ззовні.
Під чергою чотири сталих слоти під трек, що грає (в заголовку панелі видно, під що саме); порожній слот показує пунктирну заглушку зі спінером, а не стискає блок, і анімацію появи отримують лише нові картки. Коли черга порожня або в ній лише вже відданий liquidsoap трек, перша порада стає наступним треком (тоді порад три плюс він): її картка підсвічується, показує "піде наступним, якщо ніхто нічого не закине · за ~N хв" і статус (качається / готово / наступний), замість "Беру!" у неї лише "Не те" — тоді Дядько Глек скидає її з autoq і бере наступну пораду. Окремого рядка "після черги" нема.
Решта порад із кнопками "Беру!" і "Не те". Взята йде в чергу від імені того, хто натиснув, але з позначкою "порада Дядька Глека" (в черзі, в ефірі й в історії). Відхилена не повертається стільки годин, скільки AutoDj:NoRepeatHours, а її артист і сід на якийсь час слабшають (див. «Не те» і швидкий скіп).
Кнопка 🎙 біля "Закинути" пише голосове просто в браузері й ставить його в чергу як звичайний трек: заграє, коли дійде черга, з ніком автора замість виконавця. Поки пишеш — червона крапка, таймер і смужки рівня (видно, що мікрофон таки чує); ліміт Voice:MaxSeconds (2 хвилини), на ньому запис зупиняється сам. Далі запис можна послухати, перезаписати ("Ще раз") або викинути, і аж потім "Закинути в чергу". У черзі й в історії голосове видно по мікрофону замість обкладинки, кнопка ▶ поруч дає послухати його до ефіру.
Браузер шле те, що вміє (webm/opus, у Safari mp4), сервер переганяє це ffmpeg'ом у моно-mp3 просто в cache\ (там же, де треки, тож liquidsoap бачить файл без окремого монтування) і заразом підтягує гучність до Voice:LoudnessLufs — інакше голос між треками ледь чути. Файл зветься voice-<id>.mp3, і саме по префіксу voice- голосове впізнають і сервер, і фронт: зокрема Дядько Глек не бере голосове за сід — від чийогось "привіт усім" схожої музики не підбереш, тож поради лишаються від останнього справжнього треку.
Мікрофон браузер дає тільки на https (або на localhost), тож на голому http://<IP сервера>:8080 кнопки не буде.
Кнопка 🎙 у шапці — голосовий чат просто на сайті, щоб не сидіти паралельно в Діскорді. Бейдж — скільки людей зараз у голосі, панель по кліку — хто там, «Зайти», гучність кожного окремо, свій мікрофон (🎙/🔇), «нікого не чую» (🙉) і «⚙ Мікрофон і звук»: коли говорити (від голосу з порогом і шкалою рівня, поки тримаєш кнопку — клавіша чи кнопка пада, або завжди відкрито), вибір мікрофона, «притишувати радіо, коли хтось говорить». У списку людей біля того, хто в голосі, — 🎙. Заходять лише акаунти (VoiceChat:MaxPerRoom — 10 на кімнату).
Голос іде напряму між браузерами (WebRTC, кожен з кожним), сервер через себе не пропускає ні байта звуку. VoiceChat.cs лише тримає, хто в якій кімнаті голосу (подія voice усім), каже кожному, кого він чує і хто чує його (voiceMe), і передає листи між браузерами, поки ті домовляються про з'єднання (voiceSignal, лише в межах однієї кімнати, своя квота). Хаб: VoiceJoin/VoiceLeave/VoiceFollow/VoiceSet/VoiceShare/VoiceWatch/VoiceSignal. Клієнт — web/voice.js (HVoice, у консолі HVoice.stats()), обробка звуку — web/static/voice-worklet.js: голосова активація й кнопка «говорити» живуть в аудіопотоці, тож у схованій вкладці (граєш у щось на весь екран) слова не обрізає; затримка 30 мс відчиняє ворота ще до першого звуку. Позивний вкладка вигадує сама й тримає, поки відкрита: деплой голосу не рве — сервер перезапустився, браузери зайшли знову з тими самими позивними, а з'єднання між людьми весь час жили (людину, що зникла зі списку, тримаємо ще 15 с).
Голос столу. Сів за стіл на компанію (3+ місця: мафія, шпигун, Своя гра, дурень на чотирьох) — голос сам переходить у голос цього столу (t:<стіл>): хто не грає, не чує, про що домовляються за столом. Встав і пішов зі сторінки столу — сервер повертає в Посиденьки. На дуелях сам не переходиш (двоє за шахами зазвичай балакають з усіма), але на картці столу є кнопка «🎙 Говорити» / «🎙 Голос столу · N», а в панелі — «🎲 До голосу столу» і «↩ Назад у Посиденьки» (після неї той стіл уже не тягне). На чіпах гравців — 🎙 у тих, хто в голосі столу, і зелене кільце в того, хто зараз говорить. Коли переходиш, з тими, хто лишився, одразу глушишся в обидва боки, а з'єднання не рветься: за стіл переходять ті самі люди, і голос іде далі без секунди тиші. Глядач теж може зайти в голос столу («🎙 Говорити»), але посеред партії, де йому не можна писати в балачку столу (Game.TalkBlock), він говорить лише «на лаву» — з іншими глядачами, а стіл чує.
Правила ігор. Хто кого чує за столом, вирішує гра (Game.Voice(seat) → VoiceRule(Talk, Hear): людину A чує B, коли канал з A.Talk є в B.Hear); сервер шле кожному «він мене чує / я його чую», браузер слухається з обох боків — знімає свій трек (replaceTrack(null)) і глушить чужий. Типово посеред партії той, кому TalkBlock не дає писати, говорить лише на лаву. Мафія (Mafia.Voice): уночі мафія й дон шепочуться між собою, решта живих спить — ні слова, ні звуку; мертві й глядачі говорять лише на лаві, мертві при цьому чують і нічну мафію (ролі їм і так відомі), глядачі — ні. Позивні (Pozyvni.Voice): капітан мовчить (але чує), поки ходить його команда — думає над підказкою чи команда шукає слова; хід перейшов — говорить знову. У панелі тоді рядок «🌙 Зараз ти нікого не чуєш — так велить гра» / «🤫 Зараз тебе не чути» / «🪑 Ти на лаві», а біля людей — «🌙 не чути», «🤫 тебе не чує».
Смуга. Поки ти в голосі, у самій шапці (вона липка — смугу видно на будь-якій сторінці) стоїть смуга: кімната й кружечки людей з аватарками з Лавки (кільце «дихає» гучністю того, хто говорить; 🔇/🙉 — у куточку), хто зараз говорить, хто показує екран, і кнопки — мікрофон, навушники, показ екрана, ⚙ панель, «Вийти». Тебе в голосі нема, а в Посиденьках сидять — тонка смужка «Оля, Петро посиденькують · 🎙 Підсісти» (✕ ховає її, поки склад не зміниться). У грі на весь екран (body.gfull / g-imm) смуги нема — замість неї кружечок, що тягається і відкриває панель. Висоту шапки voice.js пише в --hdr-h: на ній стоять липкі балачки й меню бібліотеки, а ігри міряють шапку самі (core.js fit).
Перевірка мікрофона. «🎧 Послухати себе» (у панелі до входу й у «⚙ Мікрофон і звук»): 5 секунд запису рівно того, що летить людям (після шумодаву й воріт), і програш тобі; радіо на цей час притихає. Поруч живий рівень мікрофона з рискою порогу. Поза голосом звук запускається лише на перевірку; з вимкненим мікрофоном у голосі ворота на перевірку відчинені, але людям летить тиша.
Показ екрана. «🖥 Показати екран» у смузі чи панелі (лише де браузер уміє getDisplayMedia — на комп'ютері); хто показує, видно просто в смузі — «🖥 Петро показує · Дивитись». Дивишся на «сцені» (у шапці під смугою, на всю ширину, висоту тягнеш за нижній край) або у «віконці» поверх сторінки (тягається за смужку, розмір — за лівий верхній куток); є ще «на весь екран», «картинка в картинці» браузера (трансляцію видно й з іншої вкладки) і своя гучність трансляції. У грі на весь екран — завжди віконце. Місце й розміри пам'ятає браузер (localStorage vc). Екран летить лише тим, хто натиснув «Дивитись» (сервер: VoiceShare, VoiceWatch → watch у voiceMe), кожному глядачеві — свій потік до 2,5 Мбіт/с і 30 кадрів, тож показувати п'ятьом — це п'ять потоків із твоєї віддачі. Звук вкладки чи екрана, якщо в діалозі браузера поставити галочку, іде разом із картинкою. Доріжки екрана живуть в окремих трансиверах: перестав показувати — вони неактивні, почав знову — оживають ті самі.
Щоб пара не німіла. Кожне з'єднання має номер сесії (росте й не повторюється, навіть коли годинник відскочив), кожен лист несе обидва — свій (s) і той, який знаю в того боку (r, 0 — ще не знаю). Пару зводить лише «hello»: від старішого з'єднання — у кошик; від новішого — моє ще свіже, то знайомлюсь, а вже говорило зі старим — роблю нове, одразу знайоме з новим (його hello несе r, тож той бік, свіжий, у відповідь нічого не перестворює — петлі нема); той бік не знає мого поточного — відповідаю своїм hello. Решта листів — лише від знайомого до мого поточного, і переговори починаються лише між знайомими. Поки не домовились, hello повторюється раз на 5 с. Так вийшов-зайшов за дві секунди, телефон, що пів хвилини був без мережі, чи деплой не лишають пару німою. Першу пропозицію з'єднання робить лише один бік («нечемний», у кого позивний менший): коли обидва починали разом, Chrome після відкату переставав збирати кандидатів, і пара висіла «new». Сторож рахує час без зв'язку від миті, коли він пропав (не від народження з'єднання): чемний не дочекався пропозиції за 4 с — починає сам; домовились, а зв'язку нема 10 с (чи «хитається» 5 с) — перезапуск ICE; нема 25 с — з'єднання наново (не посеред переговорів). Показ екрана переживає деплой: вкладка заходить знову з «показую» (VoiceJoin(…, share)), глядач просить картинку знову, щойно бачить, що той повернувся.
Шумодав. «🧹 Шумодав» у налаштуваннях (типово ввімкнено): RNNoise — нейромережевий шумодав Xiph.Org (BSD-3-Clause), скомпільований у wasm проєктом Jitsi (@jitsi/rnnoise-wasm 0.2.1, Apache-2.0, web/static/rnnoise-sync.js без змін і rnnoise-LICENSE.txt). Стоїть в аудіопотоці між мікрофоном і воротами (web/static/voice-denoise.js, кадри по 480 семплів, +10 мс затримки; AudioContext на 48 кГц), поки працює — браузерний шумодав вимкнено. 1,9 МБ вантажаться лише тим, у кого він увімкнений; не завантажився — лишається браузерний. Після реконекту хаба хто не з'єднаний — перезапуск ICE. Айфон приспав звук (дзвінок, заблокований екран) — будимо, не вийшло — просимо дотику і пишемо в панелі. Список «хто де» (voice) летить не частіше ніж раз на 250 мс; позивні в ньому бачать лише ті, хто в голосі. На айфоні гучність <audio> лише для читання — повзунків гучності людей там нема.
Щоб пробитись крізь роутери, браузер питає публічні STUN (VoiceChat:IceServers, типово Google й Cloudflare). Хто сидить за «сірим» NAT провайдера (CGNAT, мобільний інтернет), той напряму не проб'ється — 01.10 перший же тест із друзями дав «з'єднуюсь… → ⚠ нема зв'язку» саме так. Тому є вбудований ретранслятор TURN (src/Hlechyky/Turn/, RFC 5766, UDP й IPv4): живе в процесі сервера, окремих програм і Docker не треба, брандмауер Windows пропускає dotnet.exe. Слухає UDP Turn:Port (3478), кімнатки ретрансляції — на UDP Turn:RelayFrom–RelayTo (49160–49359); на MikroTik — dst-nat обох на ПК. Логіни тимчасові, як у coturn use-auth-secret (строк:позивний, HMAC-SHA1 від ключа data/turn.key, що переживає перезапуски), роздає їх VoiceChat лише тим, хто зайшов у голос. Своїм із домашньої мережі (Caddy бачить адресу роутера) — адреса ретранслятора в мережі (Turn:LanHost, порожньо — сама), решті — публічна (Turn:PublicHost). Пересилає лише туди, куди браузер дав дозвіл, і ніколи — у домашню мережу; пакет між двома кімнатками самого ретранслятора (друг за NAT ↔ ти через ретранслятор) передається всередині процесу — роутер «петлю» на ці порти не робить. У appsettings.json ретранслятор вимкнено (щоб dev-копії не займали порт проду), вмикає його прод у appsettings.Local.json ("Turn": { "Enabled": true }). Деплой рве кімнатки ретранслятора: браузери, що йшли через нього, беруть новий логін і перезапускають ICE, щойно людина знову в кімнаті, — пауза ~10 с. Можна й зовнішній TURN: VoiceChat:TurnSecret + VoiceChat:TurnUrls.
Діагностика. Кожне з'єднання звітує в лог сервера (VoiceDiag → рядок «голос: Оля → Петро: {…}»): піднялось — яким шляхом (host→srflx, relay→relay…, затримка), ні (15 с чи failed) — кандидати обох боків за типами, стани пар, переговорів і ICE; IP-адрес там нема. У панелі біля людини — «через ретранслятор», коли голос іде ним. Поки сервера нема, листи між браузерами чекають у черзі; пропозиція, що висить без відповіді 8 с, відкочується й шлеться наново.
Сайт має три розділи, і кожен екран має свою адресу — F5 повертає туди ж, а посилання можна кинути в балачки:
| Адреса | Що це |
|---|---|
#efir | що зараз грає, пошук, черга і поради Дядька Глека |
#lib/<вкладка> | Бібліотека: history, likes, playlists, rating, top, bans, ads (остання — лише адмінові) |
#games | лобі ігор: живі столи з усіх груп, каталог із фільтрами |
#games/room/<id> | відкритий стіл на всю ширину змісту |
#games/profile · leaders · daily · x:<панель> | підрозділи «Ігор» |
#chat | балачки окремою вкладкою (тільки телефон) |
▶ Врубити, ❤, ⏭,
гучність, 🎧 і гаманець 🏺. Тонка смужка під шапкою показує, скільки лишилось треку. Керувати
музикою можна з будь-якого розділу; панель «В ефірі» і шапка малюються з одного джерела
(paintNowInto у web/app.js), тож лайк і скіп там ті самі.]; стан пам'ятається.
Поки згорнуто, непрочитане від людей рахується (бейдж на кнопці і «(3)» у заголовку вкладки), а коли
кличуть на ім'я — прилітає короткий тост./ — пошук, 1/2/3 — розділи, ] — балачки, Esc — назад зі столу (спершу з ⛶).
Клавішу, яку вже з'їла гра, розділи не перехоплюють.web/static/fonts, OFL, без Google Fonts). Не подобається —
--display: var(--font) у :root web/static/style.css, і все повертається на системний.ChatBefore у хабі),
аж до «Це найперше, що тут писали»; хто гортає історію, того нове повідомлення донизу не смикає.chat.topic; вибір пам'ятається),
а однакові дії однієї людини поспіль склеюються: «Smaug скіпає ×4» із кнопкою «що саме». Реклама, яку Глек
ставить щокілька треків, у Журнал не пише, а його власні невдалі завантаження йдуть лише в лог сервера.ChatFlood, спільний для Балачок, столів і агентів). Кубик і коротке «так» / «+» можна
двічі підряд.Пошук і метадані через внутрішній API YouTube Music (без ключа). Посилання: YouTube, YT Music, Spotify, і все, що вміє yt-dlp. Spotify-трек (open.spotify.com/track/…, spotify:track:…, короткі spotify.link): артист, назва і довжина беруться з JSON embed-сторінки (звичайна сторінка ботам OG-тегів більше не віддає), далі пошук на YouTube Music — та сама назва й хоч один спільний виконавець, найближча довжина, без реміксів/live/sped up, яких нема в назві оригіналу; при промаху — перший результат схожої довжини.
Альбоми й плейлисти (Albums.cs, GET /api/album?url=…, POST /api/album/queue, POST /api/album/playlist): Spotify віддає трекліст з тієї самої embed-сторінки (embed/album/<id>, embed/playlist/<id>; плейлист — перші 100 треків, подкасти пропускаються; між виконавцями там нерозривний пробіл). Spotify-альбом спершу шукається в YouTube Music цілим (пошук альбомів → сторінка MPREb_…), треки зіставляються за назвою без розділових знаків і приписок та за довжиною — так грають саме альбомні версії, а на альбом іде 2–3 запити. Що не зійшлося (і всі треки плейлиста) шукається по одному, 4 пошуки нараз, з повтором при збої й порожній видачі; тут "перший-ліпший" не береться — краще "не знайшлось", ніж зовсім інша пісня. Розібране тримається в пам'яті 30 хв (перегляд і "усе в чергу" — один розбір), нараз розбирається не більше двох альбомів. Альбом на 10–15 треків розбирається за 1–2 с, плейлист на 100 — за 10–15 с. Бувають треки, яких у YouTube Music нема в оригіналі (у вересні 2026 — Bryan Adams: лише перезаписи "Classic Version"), — вони в треклісті сірі з позначкою "нема".
Відео 18+ YouTube віддає тільки залогіненому акаунту, анонімні клієнти yt-dlp не рятують. Сервер спершу качає анонімно, і лише коли YouTube просить логін, повторює з куками: YtDlp:CookiesFile (Netscape cookies.txt, експортований розширенням типу "Get cookies.txt LOCALLY" з браузера, де залогінений акаунт з підтвердженим віком; краще окремий акаунт, не основний) або YtDlp:CookiesFromBrowser (firefox; Chrome/Edge блокують свою базу куків, поки запущені). Без куків трек 18+ в черзі позначається помилкою з поясненням. Аудіо качається bestaudio без перекодування в cache\<id>.m4a|opus.
Ефір цілодобовий, це ~200 треків і до 1 ГБ на день, тож кеш тримається в межах YtDlp:CacheMaxGb (5 ГБ). Сервер (TrackCache.cs) перевіряє його при старті, раз на 10 хвилин і після кожного скачування. Коли кеш переріс ліміт, видаляє файли до 90% ліміту, починаючи з тих, що давно й рідко грали: кожен повтор дає файлу 2 дні пільги (до 10 повторів), лайк — 3 дні, плейлист — 3 дні. Ніколи не чіпає: трек в ефірі, чергу, наступний трек Глека, голосові й рекламу (voice-*), недокачане і файли, якими користувались останні дві години. Видалений трек, якщо його знову замовлять, просто перекачається.
Перед скачуванням сервер шукає цю пісню в кеші не лише за id, а й під іншими id: YouTube віддає ту саму пісню з каналу виконавця, з «- Topic» і з кліпу. Той самий трек — це збіг виконавця й назви без регістру, розділових знаків і приписок на кшталт «(Official Video)», «[Lyrics]», «(Remastered)», плюс тривалість ±5 секунд. «(ao vivo)», «remix», «acoustic» лишаються в назві, тож живий запис не підміняє студійний.
Вкладка «Рейтинг» під чергою (/api/rating?days=7|30|3650&sort=plays|completion|listeners|likes): скільки разів трек грав, скільки в середньому дослуховують (секунди в ефірі від довжини файлу; хто дограв без 10 секунд — дограв), скільки людей слухало, лайки і скіпи. «Слухали» — ніки, у яких на сайті був увімкнений плеєр, коли трек грав (вкладка сама каже серверу про «Врубити»/«Стоп»); ETS2, VLC та інші підключення до потоку видно лише числом — пік слухачів потоку за трек. Слухачів записуємо з 13.09.2026, програвання й дослуховування — за всю історію.
«📊 Хто скільки» (#stats/<overview|music|games|time>, web/people.js) — чотири вкладки з одним перемикачем періоду (день · тиждень · місяць · весь час, київські дні):
GET /api/stats/overview?period=): підсумок періоду з міні-стовпчиками по днях (партії, закинуті пісні, репліки, час на сайті й під радіо, черепки, люди, вподобайки); звання — похвали (🏆 Переможець, 💯 Відмінник від 10 партій за тиждень, 🔥 Серійник, 🎮 Всеїдний, 💬 Балакун, 🫶 Душа компанії, 🎵 Діджей, 🦉 Нічна сова, 🐓 Ранній птах, 🏺 Скарбник, 🛍 Модник…) і підколи (😵 Невдаха, 🎰 Рулетка обібрала, 🙊 Мовчун, 🚫 Цензор…), з нічиєю на першому місці й тим, хто наступний; «⚔ Хто кого» — скільки разів один обіграв другого за одним столом (командна перемога — над кожним, хто програв, нічиї не рахуються); «🕐 Коли ми тусимо» — дні тижня × години за Києвом; «💎 Перл» — найлайкнутіша репліка (прибрані адміном не беруться); 🦄 рідкісні ачівки — ті, що мають одна-дві людини.GET /api/stats/music?period=, без реклами й прожарок Глека — voice-*), 🎧 хто слухає (час, поки грав плеєр, і скільки треків застав), ❤ хто лайкає, рейтинг треків.GET /api/stats/games?period=: партії, люди, чемпіон, хто грав найбільше) і таблиця вибраної гри — справжня <table>, тож стовпці рівні; подіум 🥇🥈🥉, % перемог, рекорд соло в своїх одиницях (час дня в Сапері, Шахах і Цеглинах — мілісекунди, Спринт — секунди, Динозавр — метри, Гео — очки, Глек-слово — спроби). Ело не має періоду — під таблицею про це сказано. Ігри на кількох з очками (Піктіонарі, Мелодія, Танки) пишуть кожному ще й рядок-рекорд solo — партією він не рахується.Профіль людини (#who/<нік>) має картку «✨ Цікавинки» (GET /api/stats/person/<нік>, 404 — ніде не бачили): з якого дня на сайті, улюблена гра, % перемог, найдовша серія, кривдник і улюблена жертва, улюблений виконавець, коли буває (години й дні тижня), суперники, рідкісні ачівки й звання цього тижня. Усе рахує Litopys.cs в пам'яті з сирих подій періоду; відповідь живе хвилину.
Повітряні тривоги й те, що в небі, — з NEPTUN (безкоштовно, без ключа). Сервер (Tryvoga.cs) опитує його один на всіх: тривоги раз на 15 с, цілі раз на 10 с; змінилось — летить у хаб tryvoga, і кожна вкладка перепитує свій район (GET /api/tryvoga?raion=&oblast=; не k — ?k= тут вхід адміна). Кожен обирає свій район (у акаунта — з будь-якого пристрою, POST /api/tryvoga/region), поки не обрав — Tryvoga:DefaultName (Житомир). Тривога в моєму районі — жовта (дрони) чи червона (ракети) плашка під шапкою й крапка на кнопці 🚨; сторінка #tryvoga — мапа районів за рівнем тривоги й цілей з курсом, слідом і колом похибки, перелік того, що летить у твоїй області. Мовчить NEPTUN довше за Tryvoga:StaleSeconds — сайт каже «не знаю», а не «тихо». Поруч із мапою завжди посилання на NEPTUN (їхня умова) і «це не офіційне оповіщення».
Архів для аналітики — data/tryvoga.db (TryvogaArchive.cs): кожна ціль (threats), кожна точка її шляху (threat_points) і кожна тривога в районі з початком і кінцем (alerts). Нічого не стирається; схема й приклади запитів — docs/tryvoga.md. Мапа районів — web/static/tryvoga-map.json, збирає data/geo/build-tryvoga.py.
Сторінка: згори одна картка статусу (мій район, «📡 За місцем», рядок «Довкола мене», коли оновлено), на ПК — мапа з «Тривогами зараз» по областях і «У небі зараз» ліворуч і «Довкола мене» праворуч, на телефоні мапа й «Довкола мене» — вкладками; цілі, що злипаються, — гуртом із числом.
🧭 Довкола мене (web/dovkola.js, до 21.10 — «Півник») — поруч із мапою, як застосунок «Флюгер»: циферблат довкола мене з кільцями 15/30/45 (або 50/100/150) км, зони кольором (тихо / тривога в районі / ціль у колі), стрілки-азимути на цілі й «летить на тебе / пройде за N км, за скільки хвилин» з курсу й швидкості. Де я — GPS браузера (лише в браузері, на сервер не йде), «📌 Я тут» на мапі або місто мого району; компас телефону крутить циферблат, звук — тумблером. Подробиці — docs/tryvoga.md.
Подвійний клік по start.cmd: liquidsoap, сервер і Caddy. stop.cmd зупиняє все троє. liquidsoap — звичайна Windows-збірка в tools\liquidsoap (її качає setup.ps1), він сам віддає потік, тож ні Docker, ні Icecast не потрібні (до 30.09.2026 були: Docker Desktop з WSL їв ~3 ГБ пам'яті й часом зависав). Змінив liquidsoap\radio.liq — start.ps1 radio (ефір замовкне на кілька секунд); restart liquidsoap не чіпає.
У копії для розробки (нема caddy.exe) той самий start.ps1 пропускає Caddy, потік тоді на http://127.0.0.1:8001/radio.mp3. Як підняти таку копію і віддати зміни, описано в CONTRIBUTING.md.
powershell -ExecutionPolicy Bypass -File D:\or\start.ps1 status
powershell -ExecutionPolicy Bypass -File D:\or\start.ps1 restart # перезбирає Release і перезапускає, черга не губиться
powershell -ExecutionPolicy Bypass -File D:\or\start.ps1 logs
powershell -ExecutionPolicy Bypass -File D:\or\start.ps1 autostart # автозапуск і автонагляд (див. нижче)
start.ps1 autostart реєструє завдання Hlechyky у Планувальнику (як у LeBot): спрацьовує при вході у Windows і далі щохвилини, запускає через Hlechyky.vbs (без блимання консолі) одну коротку перевірку start.ps1 watchdog:
/api/me — вбиває і запускає знову. Попередній лог лишається в logs\server.prev.log, там і шукати причину падіння. Падає раз у раз (5 запусків за пів години) — далі пробує раз на 10 хвилин;logs\liquidsoap.prev.log); процес живий, але потік 3 хвилини не слухає або годинник ефіру (clock.dump) стоїть дві перевірки поспіль — перезапускає (не частіше ніж раз на 5 хвилин). Годинник — бо 30.09.2026 на пробі Windows-збірка раз «замерзла» мовчки: процес живий, telnet відповідає, а потік віддає 0 байт.Пише в logs\watchdog.log тільки тоді, коли щось робить; чи нагляд живий, показує start.ps1 status. Після stop нагляд на паузі (data\stopped.flag), доки не буде start чи restart. Поки йде restart або деплой, нагляд нічого не чіпає: start/stop/restart/watchdog ходять по одному через спільний м'ютекс, а деплой видно по data\deploy.lock.
Нагляд працює лише після входу в акаунт (завдання Interactive). Щоб сайт сам піднімався після нічного перезавантаження від Windows Update, потрібен автоматичний вхід у Windows.
З 02.10.2026 — лише по команді: deploy.cmd (подвійний клік) або powershell -File deploy.ps1. Пуш у main сайт не чіпає: автодеплой вимкнено рядком Deploy:Enabled: false в appsettings.Local.json, бо він перезапускав сервер посеред партій, а 02.10 ще й викотив незапушений коміт через «зелену збірку» двотижневої давнини, яку раз віддав GitHub API. Нижче — як воно працює, коли його ввімкнути назад.
Коміт у main → GitHub Actions збирає (.github/workflows/build.yml) → збірка зелена → GitHub шле подію workflow_run на https://hlechyky.pp.ua/api/github/deploy. Сервер перевіряє підпис (HMAC-SHA256; Deploy:WebhookSecret в appsettings.Local.json мусить збігатися з секретом вебхука на GitHub) і запускає deploy.ps1 окремим процесом — той переживає перезапуск сервера, який сам же й робить.
deploy.ps1 робить те, що раніше робилося руками, тільки обережніше — і так, щоб люди за столами майже нічого не помітили:
git fetch; нема нічого нового — виходить.main є коміти, яких нема на GitHub, — виходить і нічого не чіпає: на прод іде лише те, що вже лежить в origin. Треба таки викотити як є — deploy.ps1 -Force.build.next з окремої копії коду (D:\or-deploy, git worktree), поки старий сервер працює. Прод-копію не чіпаємо: web\ сайт віддає прямо з неї, і клієнт випередив би сервер. Впала — нічого не змінилось./api/internal/busy: мультиплеєрні без Save, соло-забіги на екрані), — чекає до -WaitMinutes (10 хв), у лозі видно, хто грає. Не дочекався — нічого не чіпає й виходить із кодом 3: ще почекати — deploy.ps1 (збірка вже готова), не чекати й перервати партії — deploy.ps1 -Now (ставки повернуться).git merge --ff-only і одразу start.ps1 restart: столи заморожуються в знімок (data\tables.json), сервер зупиняється, build.next стає build\ (стара — у build.prev), новий сервер піднімає столи з тими самими id. Простій ~2–3 с; Caddy цей час притримує запити (lb_try_duration), а браузер не показує тостів, якщо зв'язку нема менше 4 с, і повторює хід, зроблений саме в мить перезапуску. Радіо не рветься./api/me. Не піднявся — git reset --hard назад і start.ps1 back (попередня збірка з build.prev).Що переживає перезапуск, а що ні, — docs/games/ARCHITECTURE.md §4.7. start.ps1 restart руками робить те саме, що крок 5 (збирає build.next із робочої копії сам).
Один деплой за раз (data\deploy.lock), усе пишеться в logs\deploy.log. Руками: powershell -File deploy.ps1. Подія про збірку, коміт якої вже входить у build\ (стара зелена збірка), нічого не чіпає.
Підстраховка вебхука. GitHub чекає відповіді 10 с і подію, що не вклалась, більше не шле, а до домашнього сайту він часом їде 6–9 с. Тому сервер раз на Deploy:PollMinutes (3 хв) сам питає GitHub API (Deploy:Repo, без токена) про найсвіжішу зелену збірку main. Якщо build\ зібрано не з неї, він запускає deploy.ps1 -Via poll, так само як вебхук (у logs\deploy.log видно, хто запустив). Кожен коміт пробується один раз (data\deploy.tried): деплой, що впав і відкотився, не перезапускає сайт по колу. Повторити можна новим комітом, Redeliver на GitHub або deploy.ps1 руками. Опитувач працює лише там, де є WebhookSecret і data\built.sha від start.ps1; вимкнути його — Deploy:PollMinutes: 0.
Вимкнути — Deploy:Enabled: false в appsettings.Local.json (підхоплюється наживо, без перезапуску); при порожньому WebhookSecret ендпоінт узагалі відповідає 404. Що прилітало від GitHub і з якою відповіддю видно в Settings → Webhooks → Recent Deliveries, там же кнопка Redeliver, якщо сервер саме лежав.
hlechyky.pp.ua куплений на nic.ua, DNS там же (name servers NIC.UA, записи @ і www типу A на зовнішню IP сервера). У name servers на nic.ua своя дата закінчення, окремо від домену: не дай їй проскочити.
Caddy (tools\caddy\caddy.exe, конфіг Caddyfile) слухає 80/443, сам бере і оновлює сертифікат Let's Encrypt (лежить у data\caddy\), проксує сайт на :8080, /radio.mp3 на liquidsoap 127.0.0.1:8001 і слухає простий http на :8000 для ETS2, www перекидає на голий домен, http на https. Лог: logs\caddy.log; якщо Caddy не стартує, причина в logs\caddy.err.log.
Caddyfile: tools\caddy\caddy.exe reload --config Caddyfile --adapter caddyfile (без розриву потоку слухачам). start.ps1 restart Caddy навмисно не перезапускає.appsettings.json налаштування (назва, ім'я DJ, ліміти, шляхи)
appsettings.Local.json секрети й місцеве: Auth:AdminNicks, Auth:AdminKey (лише дев), LastFm:ApiKey, Liquidsoap:ApiKey, DjBot:ApiKey, Deploy:WebhookSecret (не в гіті; шаблон appsettings.Local.example.json)
deploy.ps1 / deploy.cmd деплой по команді: pull main, пробна збірка, restart, відкат при невдачі (лог logs/deploy.log)
setup.ps1 перший запуск після git clone: качає yt-dlp/ffmpeg і словник, створює обидва файли з секретами
CONTRIBUTING.md як підняти свою копію і віддати зміни через Pull Request
Caddyfile https-фронт: домен, куди що проксувати
tools/caddy/ caddy.exe (https://caddyserver.com/api/download?os=windows&arch=amd64)
src/Hlechyky/ сервер (.NET 10, ASP.NET Core minimal API + SignalR + SQLite; DjBrain.cs — чат-бот)
src/Hlechyky/Mcp/ MCP-сервер для аі-агентів: POST /mcp, агент сідає за стіл нарівні з людьми (docs/games/MCP.md)
web/ фронт без збірки: index.html, app.js, static/ (style.css, fonts/ (Onest, OFL), glek.svg, icon.svg)
liquidsoap/ radio.liq, .env (ключ callback'а, порти; шаблон .env.example)
tools/liquidsoap/ liquidsoap.exe (офіційна Windows-збірка, setup.ps1 качає; у шляху не має бути «\цифра»)
tools/yt-dlp/ yt-dlp.exe, ffmpeg.exe, ffprobe.exe (setup.ps1 качає)
cache/ завантажені треки і голосові (voice-<id>.mp3)
data/ hlechyky.db (акаунти, історія, лайки, чат, черга, плейлисти, кеш Last.fm), ключі cookie, data/caddy (сертифікати)
data/words/ українські словники для словесних ігор — разом із data/questions, data/pictionary і data/telephone єдине, що з data/ лежить у гіті
data/pictionary/ words.txt — прості слова для Піктіонарі й Позивних, по темах
data/telephone/ phrases.txt — фрази-підказки для першого кроку Зіпсованого телефону
data/anthems/ вирізані уривки «Свого треку» з Лавки (mp3, один на людину; не в гіті)
web/static/anthems/ готові гімни Лавки (mp3; перегенерувати — docs/games/dev/anthems-make.py)
web/static/curses/ прокльони Лавки (mp3; перегенерувати — docs/games/dev/curses-make.py)
.github/workflows/ GitHub Actions: dotnet build на кожен push у main і кожен PR (зелений build на main → деплой, лише коли Deploy:Enabled)
data/words/ — словникиТри списки, з яких живуть Глек-слово, Віселиця та Ерудит, і всі три в гіті: uk-5.txt — 1506 п'ятилітерних
іменників, з яких береться слово дня (кожне має бути таким, щоб людина впізнала його без словника);
uk-guess.txt — 31 565 п'ятилітерних форм, які приймаються як спроба; uk-hangman.txt — 4988 іменників
5-12 літер для Віселиці. Усі — UTF-8 без BOM, LF, по слову на рядок. Великий словник для Ерудита
(uk-all.txt, ~3.4 млн словоформ, 80 МБ) у гіт не кладемо: його качає setup.ps1 з релізу
dict_uk, а сервер при першому старті збирає з нього
uk-all.db (SQLite) у фоновому потоці. Без великого словника нічого не ламається — Ерудит просто
вмикає режим «малий словник» з оскарженням ходу. Звідки взялись списки, під якою ліцензією і як їх
перегенерувати — data/words/LICENSE.txt і data/words/make-lists.py поруч.
data/pictionary/words.txt — слова для Піктіонарі й ПозивнихБлизько 1260 простих слів і коротких фраз, які можна намалювати за хвилину (а Позивні розкладають ті самі
слова на стіл п'ять на п'ять), розкладених по 13 темах
(тварини, їжа, дім, природа, транспорт, люди, одяг, тіло, спорт, музика, місця, речі, казки). Тема
починається рядком ## ключ | Назва, далі по слову на рядок. Щоб додати слово, досить дописати його в потрібну тему,
і сервер підхопить його після рестарту. Нова тема — ще й рядок у PictionaryWords.Topics, інакше її не обрати окремо.
| Ключ | Що робить |
|---|---|
Site:Name | назва в інтерфейсі |
Site:DjName, Site:DjNameGen | як звати авто-DJ (називний і родовий відмінок) |
Site:PublicStreamUrl | адреса потоку для плеєра на сайті |
Site:StreamDelaySeconds | поправка прогрес-бару на буфер потоку (burst у radio.liq: 192 КБ ≈ 8 с) |
Liquidsoap:FreezeRestartSeconds | годинник ефіру стоїть стільки секунд — сервер сам перезапускає liquidsoap (start.ps1 radio); 0 — лише наглядач раз на хвилину |
YtDlp:CookiesFile | шлях до cookies.txt залогіненого акаунту; береться лише для відео 18+ (див. "Джерела треків") |
YtDlp:CookiesFromBrowser | те саме, але читати куки з браузера (firefox); працює, лише якщо там залогінений YouTube |
YtDlp:MaxDurationSeconds | ліміт довжини треку для не-адмінів |
YtDlp:CacheMaxGb | скільки гігабайт треків тримати в cache\ (див. "Кеш треків"); 0 — без ліміту |
Voice:Enabled | голосові; false — кнопки 🎙 нема і /api/voice відмовляє |
Voice:MaxSeconds | скільки можна писати голосове (браузер зупиняє сам, сервер ріже) |
Voice:LoudnessLufs | до якої гучності підтягнути голос, щоб не тонув між треками |
AutoDj:SeedQuery | з чого стартувати авто-DJ, коли історії ще нема |
AutoDj:NoRepeatHours | скільки годин не повторювати зігране |
AutoDj:MaxSelfChain | скільки треків Глек крутить від власного вибору, перш ніж сід візьметься з людського замовлення; 0 — без стелі |
AutoDj:ArchiveEvery | кожен N-й авто-трек із власного архіву кімнати; 0 — не витягувати |
AutoDj:TasteBias | наскільки тиснути смаком кімнати (свої артисти, звичне письмо); 0 — не тиснути |
AutoDj:PreferScript |
Зміни в appsettings*.json підхоплюються без рестарту.
Нік займають один раз разом із паролем, і далі він належить лише своєму хазяїну: під чужим ім'ям не напишеш і за чужий стіл не сядеш. Хто не хоче пароля — гість: нік бере який хоче, але сервер ставить перед ним «гість » («гість Вася»), тож із зареєстрованим він не зіллється. Робить гість усе те ж, що й решта.
Google:ClientId — публічний, секрет не потрібен; порожній — кнопки нема). Свій Google — одразу вхід. Новий — «як тебе кликати?» (підставляється ім'я з Google) і акаунт без пароля. Нік, який хтось уже зареєстрував паролем, через Google не забрати: зайди паролем і прив'яжи Google у картці «Ти — …» — далі заходь будь-як. Це і є відновлення: забув пароль → зайшов через Google → поставив новий там само. Пошту з Google тримаємо довідково, листів не шлемо. У вбудованих браузерах (Instagram, Telegram) Google-вхід не працює — Google їх блокує; у звичайному Chrome/Safari все гаразд.trim+lower, що й гаманці. Зареєстрував «Влад» — і все, що лежало під «влад», твоє без жодного переносу. Хто заходив під голим ніком до появи акаунтів, після оновлення побачить картку з пропозицією закріпити його паролем; доки не закріпив — він «гість Влад».Db.MergeNick). Якщо в акаунта вже щось було, зливається: гаманець сумою, рейтинг — більшим ело й сумою партій, ачівки об'єднанням, гончарня — тим збереженням, де більше зроблено глеків. Балачки лишаються як були сказані. Безіменного «гість» не чіпаємо — він спільний./пароль Влад новий123. Відповідь бачить лише адмін; старі сесії того акаунта одразу гаснуть (у куці — сіль пароля). Свій пароль міняється в картці «Ти — …» (питає теперішній).admin дає нік із Auth:AdminNicks (без регістру) або accounts.role = 'admin'. Окремої адмінської куки нема: стару hlechyky_auth сервер не читає й стирає при першому запиті. Тож украдена сесія — це адмінка, і на це є у картці «Ти — …»: нова сіль акаунта, усі старі сесійні куки гаснуть, ця вкладка лишається в акаунті. З паролем — питає пароль (він же сіль хешу, хеш перераховується, пароль той самий); Google-акаунт — без питань.Сайт цілком керується геймпадом — і кімната з іграми, і ефір із балачками. Нічого вмикати не треба: досить натиснути будь-яку кнопку пада, і з'являється кільце фокуса та смужка підказок унизу. Миша, палець і клавіатура працюють, як працювали; торкнувся тачпада — кільце зникло, взяв пад — повернулось.
| Кнопка | Що робить |
|---|---|
| Лівий стік / хрестовина | ходити по кнопках; у швидких іграх — рух |
| Ⓐ | натиснути те, що в рамці; у полі вводу — відкрити екранну клавіатуру; у швидких іграх — дія (бомба, постріл, поворот) |
| Ⓑ | назад: закрити вікно, вийти зі столу, згорнути «на весь екран» |
| Ⓧ / Ⓨ | балачки / довідка по кнопках |
| LB · RB | розділ: Ефір · Бібліотека · Ігри |
| LT · RT | вкладка всередині розділу |
| ☰ Start | за столом — «на весь екран» |
| ⧉ Select | сховати смужку підказок |
| R3 | курсор: стік стає вказівником — ним малюють у Піктіонарі, розписують у горні й проходять полицю Ока майстра |
| Правий стік | гортати сторінку й балачки |
Текст вводиться своєю екранною клавіатурою (укр / lat / цифри, Ⓐ — літера, Ⓧ — стерти, Ⓨ — пробіл, LB/RB — розкладка, ☰ — готово). Стімівська (STEAM+X) теж працює — вона пише у сфокусоване поле.
На Деку сайт сам упізнає себе (вендор пада Valve, SteamOS у рядку браузера або екран 1280×800 на Linux),
показує 🎮 у шапці й один раз відкриває довідку з інструкцією: як додати браузер у Steam (краще Firefox із
Discover — пад бачить одразу; Chrome із Discover у Game Mode пада не бачить, поки не дозволити йому в Konsole:
flatpak --user override --filesystem=/run/udev:ro com.google.Chrome) і чому шаблон контролера має бути
Gamepad, а не «Web Browser». Подивитись ту саму довідку з комп'ютера — ?deck=1 в адресі (?deck=0 — вимкнути).
Код: web/static/pad.js + pad.css, контракт для ігор — docs/games/PROTOCOL.md §3 (pad),
стенди без пада — docs/games/dev/pad.html і dev/mock.html.
Auth:AdminNicks чи з роллю admin; на деві — /?k=dev): бан треку, прибирати і рухати будь-чиї треки, видаляти чужі плейлисти, треки довші за ліміт, /пароль.yt-dlp перестав качати з YouTube: онови tools\yt-dlp\yt-dlp.exe (yt-dlp.exe -U).YtDlp:CookiesFile у "Джерела треків". Перевірити вручну: tools\yt-dlp\yt-dlp.exe --cookies data\cookies.txt --simulate <url>.curl https://open.spotify.com/embed/track/<id> має містити __NEXT_DATA__ з entity.name; якщо Spotify змінив embed, лишається oEmbed (тільки назва без артиста).curl https://open.spotify.com/embed/album/<id> (чи embed/playlist/<id>) має містити __NEXT_DATA__ з entity.trackList; для YouTube Music розбір сторінок перевіряють фікстури в tests/Hlechyky.Tests/Fixtures/albums — впав тест AlbumsTests, значить YouTube Music переклав сторінку. Скільки знайшлось і чого нема — рядок spotify album|playlist … у YouTube Music у logs/server.log.https://hlechyky.pp.ua не відкривається, а :8080 працює: дивись logs\caddy.log. Для сертифіката Let's Encrypt має ходити на 80 або 443 ззовні (NAT на MikroTik, Windows Firewall). Перевірити зовні: https://check-host.net/check-tcp?host=hlechyky.pp.ua:443.curl --max-time 6 https://hlechyky.pp.ua/radio.mp3 -o x.mp3 має принести десятки кілобайт; якщо ні, дивись start.ps1 status і logs\liquidsoap.log; напряму: curl --max-time 6 http://127.0.0.1:8001/radio.mp3 -o x.mp3.logs\liquidsoap.log (і .prev.log після перезапуску).request.all, request.metadata <rid>, userq.queue, autoq.queue, radio.metadata через telnet на 1234.Game.TalkBlock/столи показує всі живі столи списком —
її відповідь бачиш лише ти.docs/games/dev/anthems-make.py, без чужих прав) і «Свій трек» за 3000. Виграв
партію на кількох — твій гімн звучить у всіх, хто за цим столом (гравці й глядачі); соло, кооп і нічия — без
гімну, кілька переможців — по черзі між раундами. В ефір радіо гімн не йде, а радіо в цей час притишується.
За столом видно смужку «🎺 Гімн Олі — «Трембіта»» з «⏹» і «🔇 Без гімнів» (назад вмикається на полиці гімнів).
«Свій трек»: береш пісню з пошуку радіо (сервер візьме її в кеш, до 10 нових на годину) або пісню чи відео з
телефона (до 40 МБ), повзунками — звідки й скільки (5–15 с), слухаєш ще до покупки й завантаження — платиш,
коли зберігаєш; сервер ffmpeg-ом вирізає уривок, вирівнює гучність і тримає лише його (data/anthems/),
без оригіналу. Новий уривок — не частіше разу на 2 хвилини; адмін може зняти чужий. Подробиці —
docs/games/specs/anthem.md.EMOJI_GROUPS у web/app.js.Links.Split), тож і Глек-балакун може закинути кілька за раз. Раніше друге посилання, вставлене під час "Закидаю…", губилось, а відповідь на перше стирала вже вставлене друге з поля.browse/MPREb_… і playlist?list=OLAK5uy_…; YouTube чи YT Music плейлист ?list=PL…) спершу показує трекліст під пошуком: "▶ Усе в чергу" по порядку, "🔀 Упереміш", галочками лишити тільки потрібні, "в чергу" біля окремого треку або "+ У плейлисти" — спільний плейлист сайту "Виконавець — Альбом" (такий уже є — докидає, чого бракує). У черзі кожен трек альбому — звичайне замовлення (бан, ліміт довжини, "уже в черзі"), у Журналі — один рядок на весь альбом. watch?v=…&list=… — це один трек, мікси RD… альбомом не вважаються./ ставить курсор у пошук, стрілки ходять по результатах, Enter закидає. Вкладка браузера показує, що грає; на телефоні трек видно на екрані блокування (Media Session).прибити письмо вручну: cyr, lat; порожньо — рахувати з того, що замовляють |
AutoDj:SelfSeedWeight | з якою вагою власний вибір Глека в ефірі підмішується до якорів; 0 — не зважати |
AutoDj:RadioWeight | вага радіо YouTube Music від треку (основа — схожі артисти) |
AutoDj:QualityGate | суддя якості: артист мусить бути відомий Last.fm/MusicBrainz або кімнаті |
AutoDj:MinListeners / MinListenersCyr | скільки слухачів на Last.fm достатньо без MusicBrainz (латиниця / кирилиця) |
AutoDj:FeedbackHours | скільки годин пам'ятати «Не те» і швидкі скіпи |
AutoDj:QuickSkipSeconds | скіп авто-треку раніше за стільки секунд — сигнал «не те» |
DjBot:ApiKey | ключ Anthropic; порожній — чат-бот мовчить |
DjBot:MonthlyBudgetUsd | стеля витрат на місяць; уперлось — Глек мовчить до першого числа |
DjBot:PerNickPerHour | скільки разів одна людина може смикнути Глека за годину |
DjBot:ChanceOnTrackChange, DjBot:ChanceOnLivelyChat | як часто він встряє сам |
Economy:ListenEveryMinutes, Economy:ListenReward | черепок за кожні N хвилин на сайті |
Economy:ListenDailyCap | скільки черепків на день можна набути самим лише сидінням на сайті |
Economy:WinReward, Economy:DrawReward, Economy:PlayReward | скільки дає перемога, нічия й участь у партії |
Economy:RewardedGamesPerDay | скільки партій на день у ту саму гру ще нагороджуються |
Economy:MinRewardMoves, Economy:MinRewardSeconds | коротша і за ходами, і за часом партія черепків не дає |
Economy:DailyReward | нагорода за розв'язану головоломку «Щоденного глека», коли гра не називає суму сама |
Economy:ClickerDailyCap | скільки черепків на день можна виміняти в гончарному колі |
Economy:ClickerDailyCapMax | до скількох клейма майстра можуть підняти ту денну стелю (+1 за кожні 10 клейм) |
Economy:AwardDailyCap | стеля на всі позастандартні нагороди від ігор разом |
Economy:BanPrice | скільки черепків коштує бан треку в ефірі (адміну безкоштовно); 0 — банить лише адмін |
Economy:UnbanPrice | скільки коштує викупити трек із бану (вкладка «🚫 Бан»); 0 — розбанює лише адмін |
Games:Enabled | розділ «🎮 Ігри»; false — вкладки нема (а коли Падельня ввімкнена, на її місці «🍳 Падельня»), нових столів, соло, «сісти», «почати», «ще раз» і турнірів нема — ні з сайту, ні від аі-агентів; партії, що вже йдуть, дограють |
Games:Off | окремі вимкнені ігри — id через кому ("clicker, poker"): у лобі їх нема, відкрити не можна, у балачках і статистиці назви лишаються |
Lelka:Enabled | Лелека (crash на всіх); false — нового столу не поставить, ставок не приймає (політ, що вже йде, долітає й розраховується), тайла в лобі нема (/api/me → lelka) |
Lelka:MinBet, Lelka:MaxBet | найменша й найбільша ставка Лелеки, черепків; MaxBet: 0 — без стелі (лише гаманець) |
Kavuny:Enabled | «Кавуни на ярмарку» (соло-crash з ножем); false — нового раунду не почати, тайла в лобі нема (/api/me → kavuny), раунд, що йде, догравається |
Kavuny:MinBet, Kavuny:MaxBet, Kavuny:MaxX | межі ставки (MaxBet: 0 — без стелі) і стеля множника ×2…×1000 (типово ×100: далі «віз порожній», раунд платить сам) |
Blackjack:Enabled | «Двадцять одно в Глека» (стіл і сам на сам); false — нового столу не поставить, ставок не приймає (роздача, що вже йде, догравається й розраховується) |
Blackjack:MinBet, Blackjack:MaxBet | найменша й найбільша ставка на бокс «Двадцять одно», черепків; MaxBet: 0 — без стелі (лише гаманець) |
Padel:Enabled | Падельня; false — /padel/, її хаб і API відповідають 404, картки в лобі нема, нагадування про збори мовчать. «Мої банки» (/api/padel/banks) лишаються: туди адмін переказує за продані черепки |
Slots:Enabled | автомати «🍒 Слоти»; false — «автомати на перерві»: оберт і Ворожка відмовляють, /api/me → slots: false, лобі ховає тему (окремий автомат — Games:Off) |
Slots:Bets, Slots:MaxBet | дозволені ставки (типово 10/20/50/100/200/500) і стеля ставки (0 — без стелі) |
Slots:JackpotPct, Slots:JackpotSeed | скільки % кожної ставки йде в Скарбничку Глека (типово 1) і з якої суми вона починає (10 000) |
Slots:JackpotMustHit | Скарбничка «має впасти до» цієї суми (типово 100 000): оберт, на якому вона дійшла б до межі, зриває її гарантовано; тоді ~30 % кожного внеску відкладається на наступний старт, щоб у середньому виплачувалось рівно внесене; 0 — межі нема, /api/slots/feed → поле mustHit |
Slots:OnAir | виграш Скарбнички ще й в ефірі голосом Глека (подія живої реклами); false — лише рядок у Балачках |
Slots:BuyBonus, Slots:BuyPrice | «Купити бонус» в автоматах, що його вміють (зараз — «Сліди на полиці»): типово false — кнопки нема, дія buy відмовляє; ціна — у ставках (типово 100×), дешевша за безпечну для автомата (з нею RTP купівлі не вищий за базу; у «Слідах» — 92×) підтягується до неї |
ShardShop:Buy | купівля черепків за гривні, типово false (вмикає той, хто тримає сайт, у своєму appsettings.Local.json); false — кнопок «Купити»/«Докупити» нема, нових заявок не приймає (те, що вже в обробці, можна скасувати й підтвердити). Увімкнена ще й мусить мати картку сайту — адмін вписує її в «📋 Заявки» |
ShardShop:Sell | продаж черепків сайту за гривні, типово false; false — кнопки «Продати» нема, нових заявок не приймає |
Bets:Enabled | «🎲 Ставки» в Дядька Глека на черепки; false — ні подій, ні ставок на столах (уже зроблені адмін може розрахувати чи скасувати). Ставлять лише акаунти |
Bets:Events, Bets:Tables | окремо: сторінка подій (адмін додає, найчастіше з Polymarket, і розраховує) і ставки на столах перед партією |
Bets:Margin | маржа Глека: кеф = (1 − Margin) / ймовірність |
Bets:MaxEventBet, Bets:MaxTableBet | стеля на одну людину в одній події / на одну партію столу, 🏺; 0 — без стелі |
Bets:MinBet, Bets:MinOdds, Bets:MaxOdds | найменша ставка і межі кефа (типово ×1,05…×100; нижче ×1,01 конфіг не пустить) |
Bets:Polymarket:BaseUrl, CacheSeconds, TimeoutSeconds | огляд Polymarket (сервер ходить сам, без ключа): адреса gamma-api, скільки тримати відповідь у кеші й скільки чекати |
Bets:SuggestPending | скільки пропозицій однієї людини водночас чекають адміна |
ShardShop:Rate, ShardShop:SellRate | скільки черепків за 1 грн, коли купують і коли продають |
ShardShop:Packs | пакети в гривнях (типово 50/100/250/500); ShardShop:CustomMin/CustomMax — своя сума, CustomMax: 0 — лише пакети |
ShardShop:SellMin | найменший продаж, грн |
ShardShop:PendingMax | скільки заявок одного гравця (кожного напрямку) водночас в обробці |
FeedbackFiles:MaxGb | скільки гігабайт файлів записок «💡 Розробнику» тримати в data\feedbackfiles (типово 3): що понад — найстаріше стирається. Файл до 10 МБ, до 5 на записку чи відповідь, 30 файлів і 100 МБ на годину з ніка; вибраний, але так і не надісланий, прибирається через добу |
Tryvoga:Enabled | 🚨 Тривога: false — сервер не опитує NEPTUN і не пише архів, на сайті нема ні плашки, ні кнопки |
Tryvoga:DefaultKey, DefaultOblast, DefaultName | район, поки людина не обрала свій (ключі NEPTUN; типово Житомирський район, на плашці — «Житомир») |
Tryvoga:AlertsSeconds, ThreatsSeconds, StaleSeconds | як часто питати тривоги (15 с) і цілі (10 с; NEPTUN просить не частіше ніж раз на 5 с) і за скільки без відповіді казати «дані застаріли» (180 с) |
Tryvoga:Archive, ArchivePath | писати архів цілей і тривог для аналітики (типово так, data/tryvoga.db) |
Google:ClientId | OAuth Client ID для кнопки «Вхід через Google» (console.cloud.google.com → Credentials → Web application, Authorized JavaScript origins: сайт і http://localhost:8080); порожній — кнопки нема |
?k=<AdminKey> — лише дев (dev.ps1, стенди, агенти). З порожнім Auth:AdminKey, як на проді, просто веде на /. З ключем (порівняння за сталий час) гостя заводить в акаунт «адмін» (без пароля, роль admin), а того, хто вже в акаунті, робить адміном.