Skip to content

Решта адмінських сценаріїв ​

«Свій адмінський клієнт» доводить до керування чергою й завантаження треку. Тут — усе інше, що вміє панель: редактор пісень, тексти, налаштування, групи, журнал аудиту та облікові записи.

Передбачається, що сокет уже підвищено і ви знаєте свій набір привілеїв із admin_confirmed.

Спершу спитайте, що доступно ​

Привілеї кажуть, чи має право цей адмін. Це не те саме, що чи вміє це сервер: та сама панель на іншій конфігурації матиме менше можливостей, і жоден привілей цього не змінить.

js
const { capabilities } = await fetch(`${SERVER}/api/public/config`).then(r => r.json());

// { uploadTracks, editTrackMetadata, moveTrackMode, deleteTracks, artistArts,
//   helperAdmins, adminAccount, ipModeration, editSettings, auditLog, history,
//   jingles, backgroundMusic, phrases }

Кнопку варто показувати лише коли виконуються обидві умови:

js
const canShowEditor = privileges.includes('editor_meta') && capabilities.editTrackMetadata;

Можливість буває вимкнена з двох різних причин. Або конфігурації бракує чогось технічного — джингли без STREAM_MODE вставляти нікуди. Або сховище не переживає перезапуск: на хостингу з EPHEMERAL_STORAGE=true і локальними файлами завантаження спрацювало б, а потім зникло при наступному деплої, тож сервер його не пропонує. Подробиці — у «Ефемерний хостинг».

Це не лише про інтерфейс

Роути перевіряють те саме незалежно від клієнта. Прихована кнопка нічого не захищає — недоступна можливість дає 400 з поясненням у error. Тож capabilities — це спосіб не показувати те, що однаково не спрацює, а не механізм захисту.

Редактор пісень ​

Договір ендпоінта коротко: POST /api/admin/song-editor/save приймає metadata, lyricsEntry і offset разом із трьома прапорцями (metadataChanged, lyricsChanged, offsetChanged) — сервер перевіряє привілей саме за тим, що справді змінюється (editor_meta для метаданих, editor_lyrics або editor_meta для тексту), а зайнятий ефіром трек (поточний або наступний) відхиляється 409 з поясненням у error.localized.

Повна реалізація — з тим, як саме клієнт вирішує, що з трьох речей змінилось, як поводиться з форматом таймкодів і що показує на 409 — описана окремо: «Адмінський редактор пісень».

Тексти й таймкоди ​

POST /api/admin/upload-song-lyrics шукає текст за title, artist, необов'язковими album і duration. Доступ: upload_songs або editor_lyrics; editor_meta окремо не дозволяє пошук. Відповідь містить lyricsEntry та lyricsStatus (none, plain, synced). Пошук сам по собі текст не зберігає. Групове оновлення використовує цей пошук і POST /api/admin/song-editor/save.

Тексти живуть окремо від треків і ключуються парою виконавець + назва, а не ідентифікатором треку. Через це перейменування виконавця в метаданих переносить і текст.

Що потрібноЕндпоінт
Легкий перелік, для чого текст уже єGET /api/admin/lyrics/cache-index
Текст одного трекуGET /api/admin/lyrics/cache-entry
Зберегти відредагованийPUT /api/admin/lyrics/cache
Забути (щоб перезапитати)DELETE /api/admin/lyrics/cache
Усі зсувиGET /api/admin/lyrics/offsets

GET /api/admin/lyrics/cache-full віддає геть усе одним шматком — на великій фонотеці це важка відповідь, тож для списку беріть cache-index.

Для звірки таймкодів є GET /api/admin/lyrics/audio-preview: повертає посилання, яким можна годувати <audio src> напряму. У хмарі це тимчасове посилання на бакет, локально — посилання на адмінський стрім-роут із токеном у запиті. Клієнту різниця не видна.

Зсув зберігається окремо, і в його шляху немає /admin, хоча адмінський токен потрібен:

js
await post('/api/lyrics/offset', { title, artist, offset: -0.4 });

Зсув у секундах і може бути від'ємним.

Налаштування ​

GET /api/admin/settings доступний будь-якому автентифікованому адміну. POST — теж, але авторизація йде посекційно, за тим, що реально відрізняється від збереженого:

СекціяПривілей
brandingsettings_branding
generationsettings_algorithm
radioHostsradio_moderator

Панель завжди шле всі секції цілком, тому перевірка не може спиратися на те, що надіслано — інакше кожен адмін мусив би мати всі три привілеї. Порівнюється саме зміст.

Поле songGroups у цьому тілі ігнорується: групи мають власні ендпоінти.

Якщо capabilities.editSettings вимкнено, GET усе одно працює. Це навмисно: бачити, що радіо робить зараз, корисно й тоді, коли змінити нічого не можна — референсна панель у такому разі показує форму лише для читання.

Групи пісень ​

Група — це іменований набір треків одного режиму, який можна поставити в чергу одним рухом.

js
await post('/api/admin/song-groups', {
  name: 'Ранковий блок',
  mode: 'day',
  songs: ['day/Artist - Title.mp3', '…'],
});

Читати групи може будь-який адмін; змінювати — з settings_groups. Для пошуку треків при наповненні є GET /api/admin/song-groups/library з посторінковістю.

Постановка групи в чергу потребує обох привілеїв — queue_manage і settings_groups, — бо дія торкається і ефіру, і визначення груп. Одного не досить, і подія admin_insert_song_group робить те саме з тими самими вимогами, тож правило не обійти через сокет.

Журнал аудиту ​

Формат розгорнутих змін «Було / Стало», перелік операцій і доповнення контрактів API описані в детальному журналі адміністрування.

js
const { entries, total } = await get('/api/admin/audit?window=24h&offset=0&limit=50');

Вікна: 1h, 6h, 24h, 7d. Журнал читається з кеша в пам'яті, тож посторінковість дешева.

Записи створюються сервером самостійно — окремого «запиши в аудит» ендпоінта немає й бути не може. Кожен запис має operationType, adminLogin і data; склад data залежить від типу операції.

Розділ вимикається цілком, коли сховище не переживає перезапуск: журнал писався б у те, що зникне при наступному деплої, і показував би оманливу частину того, що насправді відбувалося.

Історія програвань ​

js
const { entries, total } = await get('/api/admin/history');

На відміну від журналу аудиту, тут потрібен привілей stats — окремого під історію не заводили, це той самий, що й для статистики. Пагінації немає: обсяг і так обмежений тим самим LOG_RETENTION_DAYS, що й аудит.

Є і публічний варіант без жодної авторизації — останні 10 записів, ним користується і сам клієнт радіо, і сторонні застосунки:

js
const recent = await fetch(`${SERVER}/api/history`).then(r => r.json());

Вимикається за тим самим правилом, що й аудит: писати в те, що не переживе перезапуск, немає сенсу.

Облікові записи ​

Дві незалежні гілки з різними вимогами.

Помічники — лише супер-адмін ​

js
const { admins, allPrivileges } = await get('/api/admin/admins');
await post('/api/admin/admins', { login: 'helper', password: 'Temp1234', privileges: ['stats'] });

Пароль тут тимчасовий. Обліковий запис створюється з authorized: false і не вміє нічого, доки помічник сам його не активує. Хеші паролів у відповідях не повертаються ніколи.

Зміна привілеїв застосовується без повторного входу: якщо помічник онлайн, він одразу отримує подію privileges_updated. Видалення надсилає йому force_logout з reason: "admin_deleted", і сесія завершується негайно.

Власний обліковий запис ​

js
await post('/api/admin/admins/self/activate', { tempPassword, newPassword });
await put('/api/admin/admins/self/login',    { newLogin, currentPassword });
await put('/api/admin/admins/self/password', { currentPassword, newPassword });

Активація — єдина дія, доступна помічнику з authorized: false. Після успіху приходить подія admin_authorized.

Супер-адмін тут виняток

Його логін і пароль задаються змінними оточення (ADMIN_LOGIN, ADMIN_PASS), тож гілка /self для нього відповідає 400. Змінити їх можна лише в конфігурації сервера.

Донати ​

Донат — це платне замовлення пісні, що оминає модерацію: чергу двигає лише сервер після підтвердженої оплати, ніколи клієнт напряму. Розділ вимикається цілком, коли capabilities.donations не вмикається — жодного активного провайдера не налаштовано, або сховище не переживає перезапуск, за тим самим принципом, що й журнал аудиту.

Читання налаштувань доступне будь-якому адміну, як і GET /api/admin/settings; запис, і перегляд історії — лише з привілеєм donations_manage:

js
const { settings, provider, historyCurrencies } = await get('/api/admin/donations/settings');
await post('/api/admin/donations/settings', {
  ...settings,
  tiersEnabled: true,
  tierCeiling: 5,
});

Два типи провайдерів, дві поведінки клієнта ​

provider (з тієї самої відповіді, і з donationInfo у /api/public/config) каже, чого чекати від POST /api/public/donations/create — гілкуйтеся за flowType, а не за конкретним provider.id, бо реалізацій буде більше:

flowTypeПровайдериЩо робити з відповіддю
checkoutLiqPay, Stripeодразу location.href = redirectUrl
matchingDonatello, Ko-fiпоказати pageUrl і matchCode, чекати на donation_result або опитувати /donations/:id/status

matching існує, бо ці два сервіси не мають API «створити платіж на суму X»: донатер сам платить на своїй сторінці й сам вписує код у коментар. Точної суми ніхто не форсує, тож донат приймається за принципом «не менше очікуваного», а не «рівно стільки».

Черги — це не окремий вибір, а множник

Коли tiersEnabled, кожна наступна черга коштує вдвічі дорожче за попередню, а ціна першої — це fixedPrice або pricePerSecond × тривалість, залежно від pricingMode. tierCeiling — стеля кількості черг (2-10), і сервер сам знижує її при збереженні, якщо найдорожча черга перевищує ліміт транзакції провайдера — про це скаже clamped: true у відповіді на POST.

Історія ​

js
const { entries, total } = await get('/api/admin/donations/history?window=7d&limit=50');

Вікна ті самі за формою, що й у журналі аудиту (24h, 7d), плюс 30d і max — останнє обмежене DONATION_RETENTION_DAYS (типово 365 днів, а не LOG_RETENTION_DAYS: донати зберігаються довше за журнал дій). Кожен запис має status: pending, paid, paid_unqueued, failed або expired — paid_unqueued означає, що оплату прийнято, але вставити пісню в чергу не вдалося, і це вимагає ручного розбору.

Донат і пауза черги (RADIO_HOSTS_MODE) взаємно виключні

Поки в черзі є хоч одна донатна пісня, host_pause_queue відхиляється — ведучий побачить host_queue_pause_state з denied: true. І навпаки: якщо черга вже на паузі, а прапорець blockDonationsWhileChatting вимкнено (типово), успішний донат сам знімає паузу, щоб оплачена пісня не чекала «Just Chatting» без кінця.

Що далі ​

  • Повна конфігурація — від чого залежить кожна можливість
  • Привілеї — повний перелік і що саме кожен відкриває
  • REST API — точні тіла запитів, відповідей і кодів помилок