SYSTEM NA ANTENIE

Platforma hostingu radia
bez przerw w sygnale

Agent Node.js prowadzi wiele stacji naraz: miksuje dźwięk, obsługuje DJ-ów na żywo, emituje reklamy i raportuje odtworzenia. Jedno połączenie ze słuchaczem trwa cały czas — utwory, zapowiedzi, wejścia DJ-a i bloki reklamowe wchodzą wewnątrz tego strumienia.

~5 700

linii kodu

28 modułów, zero zależności natywnych

102

endpointy REST

całość sterowalna z zewnątrz

0

zerwań przy przejściach

zmierzone w testach E2E

01 — Ścieżka sygnału

Jak dźwięk płynie przez system

To jest rdzeń całej platformy. Każde źródło dekodowane jest do surowego PCM 16-bit, trafia do miksera, a stamtąd do enkoderów. Enkoder i połączenie ze słuchaczami żyją bez przerwy — dlatego zmiana utworu, wejście DJ-a czy dżingiel nie zrywają odtwarzania.

ŹRÓDŁA MIKSER PCM ENKODERY WYJŚCIA Playlista / rotacjaffmpeg → PCM DJ na żywoport stacji + 2 Mikrofon z paneluWebSocket /live Relay / YouTubeyt-dlp → ffmpeg Reklamy, dżinglebloki + cart wall Zapowiedzi TTSpiper / espeak-ng MIKSER crossfade między utworami ducking pod głosem (overlay) wyrównanie głośności (gain) punkty cięcia ciszy (cue) sygnał obecny MP3 / AAClibmp3lame, aac OGG / Opus / FLACtylko Icecast H.264 + AACobraz + dźwięk Archiwum MP3segmenty 1 h sc_servSHOUTcast DNAS Icecastmount /radio.ogg YouTube LiveRTMP Dyskarchive/*.mp3 SŁUCHACZE — połączenie nieprzerwane
Klucz: enkoder i połączenie źródła z serwerem nadawania powstają RAZ przy starcie stacji. Wszystko inne dzieje się przed nimi, w mikserze.

Dwa tryby wejścia na antenę

TrybCo robiKiedy używać
duck
domyślny
Muzyka gra dalej, ściszona pod głosem. Po zejściu wraca do pełnej głośności — ten sam utwór, bez przeskoku. Przejścia głośności płynne. Prowadzenie programu, wejścia między utworami, komentarz na podkładzie
replace Głos całkowicie zastępuje muzykę (crossfade), po zejściu startuje kolejny utwór. Długie audycje, transmisje, gdy podkład przeszkadza

Ustawiane w autopilotConfig.djMode, głośność podkładu w autopilotConfig.djDuck (domyślnie 0.15).

02 — Mapa modułów

Co za co odpowiada

Każdy moduł ma jedną odpowiedzialność. Strzałka = kto kogo woła.

WARSTWA HTTP app.js Express · montowanie tras · WebSocket /live · serwowanie panelu i apki · sprzątanie sierot przy starcie routes/stationCRUD stacji, diag routes/autopilotantena, DJ, carts routes/libraryskan, raporty routes/adskampanie routes/requestsżyczenia routes/files · playlists · nowplayingpliki, listy, widget SILNIK autopilotEngine.js sekwencer · ramówka · rotacje · DJ · reklamy · kolejka audioPipeline.js mikser PCM · crossfade · overlay processManager.js start/stop sc_serv · sieroty · diagnoza USŁUGI musicLibrarySQLite, cue, log adsSchedulerbloki, kampanie announceTTS polski mediaEnrichokładki, teksty youtubeSourceyt-dlp configGeneratorsc_serv.conf watchdog · binarySetupsamonaprawa WYJŚCIE shoutcastSource.js icecastSource.js djIngest.js
Bursztyn = rdzeń audio. Zieleń = wejście/wyjście sieciowe. Szary = warstwa sterująca.

autopilotEngine.js 1273

Serce systemu. Decyduje co gra teraz: czyta ramówkę, generuje rotacje z regułami no-repeat, wstrzykuje bloki reklamowe, obsługuje wejścia DJ-a, prowadzi kolejkę.

audioPipeline.js 701

Miksuje PCM i utrzymuje enkodery. Tu żyje crossfade, ducking, archiwum i wszystkie wyjścia (SHOUTcast, Icecast, RTMP).

processManager.js 377

Cykl życia sc_serv: start, stop, wykrywanie osieroconych procesów, checklista diagnostyczna dla panelu.

musicLibrary.js 366

Baza SQLite (node:sqlite, bez kompilacji): metadane, punkty cięcia, wyrównanie głośności, log odtworzeń dla ZAiKS i reklamodawców.

03 — Drzewo plików

Gdzie co leży

scscript-agent/ ├─ app.js wejście: Express, trasy, WebSocket, start usług ├─ panel.html panel www — jeden plik, bez frameworka ├─ app-mobile.html apka PWA (telefon + Electron) ├─ package.json 9 zależności, zero natywnych │ ├─ services/ logika biznesowa │ ├─ autopilotEngine.js sekwencer anteny │ ├─ audioPipeline.js mikser PCM + enkodery + archiwum │ ├─ processManager.js sc_serv: start/stop/diagnoza/sieroty │ ├─ musicLibrary.js SQLite: tagi, cue, gain, log odtworzeń │ ├─ configGenerator.js generuje sc_serv.conf (DNAS 2.6) │ ├─ shoutcastSource.js klient źródła SHOUTcast v1 │ ├─ icecastSource.js klient źródła Icecast (HTTP PUT) │ ├─ djIngest.js serwer portu DJ-ów │ ├─ adsScheduler.js bloki i kampanie reklamowe │ ├─ announce.js TTS + odmiana godzin po polsku │ ├─ mediaEnrich.js okładki, teksty, teledyski │ ├─ youtubeSource.js yt-dlp → strumień audio │ ├─ requests.js życzenia słuchaczy │ ├─ watchdog.js restart padniętych procesów │ ├─ binarySetup.js auto-kopiowanie sc_serv + cacert.pem │ ├─ nowPlayingPoller.js odpytywanie DNAS o słuchaczy │ └─ cluster.js praca wielowęzłowa (opcjonalna) │ ├─ routes/ warstwa HTTP — cienka, bez logiki │ └─ station · autopilot · library · ads · requests · files · playlists · nowplaying · shoutcast · internal │ ├─ middleware/auth.js tokeny i role (master/owner/editor/viewer) ├─ bin/ sc_serv na 4 platformy + cacert.pem │ └─ win64/ win32/ linux64/ linux32/ ├─ public/app/ manifest PWA, service worker, ikony ├─ deploy/ │ ├─ install.sh Linux: Node 22, ffmpeg, espeak-ng, systemd │ ├─ setup-ssl.sh nginx + Let's Encrypt (panel + streamy) │ ├─ install-windows.bat Windows: ffmpeg, tokeny, sc_serv │ └─ start-windows.bat auto npm install + start └─ desktop/ Electron: instalatory Win/Mac
04 — Dane

Gdzie system trzyma stan

Wszystko per stacja, w jednym katalogu. Brak centralnej bazy — stację można przenieść na inny serwer kopiując folder.

stations/station_<id>/ ├─ station.json konfiguracja stacji + konta DJ + token ├─ sc_serv.conf GENEROWANY — nie edytuj ręcznie, nadpisze się ├─ schedule.json ramówka: eventy + rotacje ├─ ads.json bloki i kampanie reklamowe ├─ carts.json pady cart wall ├─ requests.json życzenia: oczekujące + historia ├─ library.db SQLite: utwory + log odtworzeń ├─ nowplaying.json cache "teraz gramy" dla widgetu ├─ media/ wgrana muzyka, spoty, dżingle, voicetracki ├─ playlists/ *.lst — jedna ścieżka na linię ├─ archive/ nagrania anteny (segmenty godzinne) ├─ cache/ okładki i metadane z sieci └─ logs/ sc_serv.log, sc_w3c.log, sc_serv_process.log

Tabele w library.db

TabelaZawieraKto pisze
tracks ścieżka, artysta, tytuł, album, rok, gatunek, ocena, tagi, długość, cue_start / cue_end / mix_point / gain_db, dayparting, daty grywalności, licznik odtworzeń skan biblioteki, edycja w panelu
play_log plik, artysta, tytuł, czas emisji, długość, źródło + campaign_id, campaign_name, client, block_time dla reklam silnik przy każdym odtworzeniu
Dlaczego node:sqlite: wbudowany w Node 22+, więc paczka nie wymaga kompilacji C++. Poprzednio używany better-sqlite3 wymagał Visual Studio na Windows i blokował instalację.
05 — Cykl życia

Co dzieje się od kliknięcia „Start"

#KrokSzczegóły
1WalidacjaSprawdzenie configu i binarki. Hasła admin ≠ source (inaczej DNAS odmawia startu).
2SprzątanieZabicie osieroconych sc_serv z poprzedniej sesji, które trzymają porty.
3Start sc_servSpawn procesu, zapis PID, czekanie do 6 s aż realnie wstanie.
4PipelineUruchomienie enkoderów i połączenie źródeł z serwerem nadawania.
5Port DJNasłuch na porcie stacji + 2 dla zewnętrznych prowadzących.
6RamówkaTimer sprawdzający eventy co 20 s (z losowym przesunięciem 0–15 s, żeby 100 stacji nie przełączało się w tej samej sekundzie).
7Pierwszy utwórWypełnienie kolejki wg ramówki i start odtwarzania.

Samonaprawa

Watchdog

Co 10 s sprawdza czy sc_serv żyje. Padnięty — restartuje i notuje w logu.

Sieroty

Na Windows sc_serv.exe przeżywa zamknięcie agenta i blokuje port. Przy starcie agent je znajduje po PID-ach i ubija.

Binarki

Brak sc_serv w katalogu roboczym — agent kopiuje właściwą wersję (system + architektura) i dokłada cacert.pem.

Reconnect

Zerwane połączenie z serwerem nadawania odtwarzane z narastającym opóźnieniem (2 s → 30 s), bez restartu enkodera.

06 — Referencja API

Wszystkie endpointy

Kompletna specyfikacja: 102 endpointy z ciałami żądań i odpowiedzi. Rozwiń pozycję, żeby zobaczyć szczegóły. Filtr niżej szuka po ścieżce i opisie.

Autoryzacja

Authorization: Bearer <token>
RolaSkądZakres
masterAGENT_MASTER_TOKEN w .envwszystkie stacje na agencie
ownerzwracany przy POST /stationsjedna stacja, pełne prawa
editorGET /stations/:id/tokensobsługa codzienna — bez konfiguracji, kasowania i tokenów
viewerGET /stations/:id/tokenstylko GET

Tokeny są bezstanowe (HMAC z AGENT_SECRET). Unieważnienie wszystkich naraz = zmiana sekretu i restart agenta.

Odpowiedzi i błędy

{ "success": true,  "...": "dane" }
{ "success": false, "error": "opis co poszło nie tak" }
KodZnaczenie
400złe dane wejściowe — error mówi które
401brak lub zły token
403token poprawny, rola bez uprawnień
404stacja lub zasób nie istnieje
409konflikt stanu (np. już działa)
500błąd serwera — szczegóły w logu agenta
01Stacje
GET/stationslista stacji na agencie
{ "success": true, "stations": [
  { "stationId": "radio1", "enabled": true, "running": true, "port": 8000 }
]}
POST/stationsutworzenie stacji
Żądanie
{
  "stationId": "radio1",
  "scServConfig": {
    "port": 8000,
    "adminPassword": "haslo-admina",
    "sourcePassword": "haslo-source",
    "streamTitle": "Radio Przykład",
    "maxListeners": 100,
    "publicServer": "never",
    "authhash": ""
  },
  "autopilotConfig": {
    "bitrate": 128, "crossfade": 2, "xfadeThreshold": 10,
    "shuffle": true, "djMode": "duck", "djDuck": 0.15, "djFade": 1.5,
    "serverType": "shoutcast", "diskQuotaMB": 5000,
    "archive": { "enabled": false, "keepDays": 14 },
    "outputs": []
  }
}
Odpowiedź
{ "success": true, "stationId": "radio1", "token": "…" }
Hasła muszą się różnić. DNAS odmawia startu przy identycznym adminPassword i sourcePassword — endpoint zwraca wtedy 400. Zapisz zwrócony token: to dostęp owner do tej stacji.
Pola scServConfig
PoleTypDomyślnieOpis
portint8000port dla słuchaczy; port DJ = port + 2
adminPasswordstringpanel admina DNAS
sourcePasswordstringlogowanie źródeł
streamTitlestring''nazwa stacji (nagłówek ICY)
maxListenersint100sloty — ilu słuchaczy naraz
publicServerenumnevernever / always — katalog SHOUTcast
authhashstringklucz z shoutcast.com dla katalogu
autodumpSourceTimeint30s bez danych → rozłączenie źródła
Pola autopilotConfig
PoleTypDomyślnieOpis
bitrateint128kbps wyjścia
crossfadefloat2sekundy nakładania utworów
xfadeThresholdint10pliki krótsze niż X s bez crossfade (dżingle)
shufflebooltruelosowa kolejność
djModeenumduckduck = muzyka gra pod głosem; replace = zastępuje
djDuckfloat0.15głośność muzyki pod głosem DJ-a
djFadefloat1.5s przejścia przy wejściu i zejściu
djPortintport+2port dla zewnętrznych DJ-ów
normalizeboolfalseloudnorm w locie (gdy brak skanu biblioteki)
recordDjboolfalsenagrywanie audycji DJ-ów
archiveobiektnull{ enabled, bitrate, keepDays }
outputstablicawyjścia — patrz grupa 10
GET/stations/:id/statuskonfiguracja i stan procesów
{ "success": true, "stationId": "radio1",
  "processes": { "scServ": { "running": true, "pid": 16212 },
                 "autopilot": { "running": true } },
  "meta": { "scServConfig": { … }, "autopilotConfig": { … } },
  "publicStreamBase": "https://domena/s" }

publicStreamBase ustawia setup-ssl.sh — panel buduje z tego link po HTTPS.

PUT/stations/:id/configzmiana konfiguracji
{ "scServConfig": { … }, "autopilotConfig": { … } }

Oba pola opcjonalne. Zmiany w scServConfig wymagają restartu stacji. Walidacja haseł jak przy tworzeniu.

tylko owner / master

DELETE/stations/:idusunięcie stacji

Usuwa konfigurację. Pliki mediów zostają na dysku.

tylko owner / master

POST/stations/:id/starturuchomienie: sc_serv + autopilot
{ "success": true, "scServ": { "pid": 16212 }, "autopilot": { "running": true } }

Sekwencja: sprzątnięcie osieroconych procesów → spawn sc_serv → czekanie do 6 s na gotowość → start pipeline'u i autopilota.

POST/stations/:id/stopzatrzymanie stacji

Słuchacze tracą połączenie.

POST/stations/:id/restartrestart — wdraża zmiany konfiguracji
POST/stations/:id/sc_serv/startstart samego serwera nadawania
POST/stations/:id/sc_serv/stopstop samego serwera nadawania
POST/stations/:id/sc_serv/restartrestart samego serwera nadawania
GET/stations/:id/diagdiagnostyka: dlaczego stacja nie startuje
{ "success": true, "diag": {
  "ok": false, "platform": "win32", "arch": "x64", "port": 8000,
  "checks": [
    { "name": "Binarka sc_serv", "ok": true,  "detail": "C:\\…\\bin\\sc_serv.exe" },
    { "name": "Plik konfiguracji", "ok": true, "detail": "…/sc_serv.conf" },
    { "name": "Osierocony sc_serv", "ok": false,
      "detail": "pid 16212 z poprzedniej sesji trzyma port",
      "hint": "Restart agenta sprzątnie go automatycznie" }
  ]
}}

Każdy nieudany punkt ma hint z konkretną instrukcją naprawy.

POST/stations/:id/clear-lockodblokowanie zaciętego startu
GET/stations/:id/logs/:typelogi

:type = sc_serv | sc_w3c | sc_serv_process

GET/stations/:id/tokenstokeny ról
{ "success": true, "tokens": {
  "owner":  "…", "editor": "…", "viewer": "…"
}}

tylko owner / master

GET/stations/:id/exporteksport konfiguracji stacji do JSON
02Antena (autopilot)
GET/stations/:id/autopilot/statuspełny stan anteny
{ "success": true, "autopilot": {
  "running": true,
  "currentTrack": "/…/media/utwor.mp3",
  "trackElapsedSec": 47,
  "trackDurationSec": 213,
  "queue": ["/…/next1.mp3", "/…/next2.mp3"],
  "queueLength": 24,
  "activePlaylist": "main",
  "currentEvent": { "name": "Poranek", "type": "block" },
  "liveDj": { "name": "maciek", "priority": 5, "since": "2026-07-23T08:12:00Z" },
  "overlayActive": false,
  "manualRelay": null,
  "pipeline": { "outputs": [
    { "name": "out1-mp3-128k", "type": "shoutcast",
      "connected": true, "bytesOut": 8412300 }
  ]}
}}

queue zawiera 10 najbliższych pozycji — używane przez voice tracking do wyboru miejsca wstawki.

POST/stations/:id/autopilot/starturuchomienie samego autopilota
POST/stations/:id/autopilot/stopzatrzymanie autopilota (serwer nadal działa)
POST/stations/:id/autopilot/nextprzeskoczenie utworu z crossfade
POST/stations/:id/autopilot/reloadprzeładowanie playlist z dysku
GET/stations/:id/autopilot/queuepodgląd kolejki
PUT/stations/:id/autopilot/configzmiana ustawień autopilota bez restartu sc_serv
POST/stations/:id/autopilot/queue/pushdodanie do kolejki
{ "tracks": ["/pełna/ścieżka.mp3"], "next": true }

next: true — zaraz po bieżącym utworze. false — na koniec kolejki.

POST/stations/:id/autopilot/queue/insertwstawienie na pozycji
{ "tracks": ["/…/voicetrack.webm"], "index": 2 }
{ "success": true, "index": 2, "queueLength": 25 }

index: 0 = zaraz po bieżącym utworze. Podstawa voice trackingu.

POST/stations/:id/autopilot/relaynadawanie z sieci lub YouTube
{ "url": "https://www.youtube.com/watch?v=…" }
{ "url": "http://inne-radio:8000/stream" }

Linki YouTube / SoundCloud / Twitch agent rozwiązuje przez yt-dlp do bezpośredniego strumienia audio. Działa też z transmisjami na żywo. Zwykły film po zakończeniu oddaje antenę playliście.

Prawa: nadawanie cudzych nagrań wymaga licencji. To decyzja operatora, nie blokada techniczna.
DELETE/stations/:id/autopilot/relaypowrót do playlisty
GET/stations/:id/autopilot/youtube/checkczy yt-dlp jest zainstalowany
{ "success": true, "ok": true, "version": "2026.07.01" }
{ "success": true, "ok": false }
03Ramówka i rotacje
GET/stations/:id/autopilot/scheduleodczyt ramówki
PUT/stations/:id/autopilot/schedulezapis ramówki
{
  "defaultPlaylist": "main",
  "events": [ … ],
  "rotations": { … }
}

defaultPlaylist przyjmuje nazwę playlisty albo rotation:nazwa.

Pasmo czasowe
{ "name": "Poranek", "playlist": "poranna",
  "startTime": "06:00", "endTime": "10:00",
  "days": [1,2,3,4,5], "priority": 1 }

days: 0 = niedziela … 6 = sobota; brak pola = codziennie. Okna przez północ (22:0002:00) działają. Przy nakładaniu wygrywa wyższy priority.

Relay o czasie
{ "type": "relay", "name": "Wiadomości", "url": "http://…:8000/;",
  "startTime": "18:00", "endTime": "18:30", "days": [1,2,3,4,5] }
Spot punktowy
{ "type": "spot", "name": "Hejnał", "file": "/…/hejnal.mp3",
  "startTime": "12:00", "mode": "queue" }

mode: queue = po bieżącym utworze, now = natychmiast z fade.

Co godzinę
{ "type": "spot", "name": "Dżingiel", "file": "/…/jingiel.mp3",
  "hourly": true, "minute": 0, "mode": "queue" }
Rotacje
"rotations": {
  "standard": {
    "template": [
      { "category": "muzyka", "count": 3,
        "filters": { "genre": "rock", "yearFrom": 2000, "yearTo": 2020,
                     "ratingMin": 3, "durMin": 120, "durMax": 360,
                     "tagsAny": "hit,lato" } },
      { "category": "jingle", "count": 1, "ignoreRules": true }
    ],
    "rules": {
      "trackNoRepeatMin": 240,
      "artistSepMin": 60,
      "titleSepMin": 120,
      "albumSepMin": 45,
      "sameTimeWindowMin": 60
    },
    "relatedArtists": { "Anna Nowak": ["Ania N.", "A. Nowak"] },
    "cycles": 12
  }
}
RegułaDziałanie
trackNoRepeatMinten sam plik nie wraca przez X minut
artistSepMinseparacja artysty — rozpoznaje feat. i aliasy z relatedArtists
titleSepMinseparacja tytułu — ignoruje dopiski typu (Remix)
albumSepMinseparacja albumu (0 = wyłączona)
sameTimeWindowMinnie graj tego, co wczoraj o tej porze

Przy braku materiału spełniającego reguły generator schodzi łagodnie — wybiera utwór najdawniej grany zamiast przerywać.

POST/stations/:id/autopilot/schedule/eventdodanie pojedynczego eventu
DELETE/stations/:id/autopilot/schedulewyczyszczenie ramówki
04Playlisty i pliki
GET/stations/:id/playlistslista playlist
GET/stations/:id/playlists/:namezawartość playlisty
{ "success": true, "tracks": ["/…/a.mp3", "/…/b.mp3"] }
PUT/stations/:id/playlists/:namezapis całej playlisty
{ "tracks": ["/…/a.mp3", "/…/b.mp3"] }

Zapisuje całą listę — to również sposób na zmianę kolejności. Format na dysku: playlists/<name>.lst, jedna ścieżka na linię.

DELETE/stations/:id/playlists/:nameusunięcie playlisty
POST/stations/:id/playlists/:name/tracksdopisanie jednego utworu
{ "path": "/…/c.mp3" }
GET/stations/:id/playlists/calendar/xmleksport ramówki do XML (zgodność z narzędziami zewnętrznymi)
PUT/stations/:id/playlists/calendar/xmlimport ramówki z XML
GET/stations/:id/fileslista plików

Parametr ?type=media

POST/stations/:id/filesupload plików

multipart/form-data, pole files — wiele plików naraz. Parametr ?type=media.

Dozwolone: .mp3 .aac .ogg .flac .wav .m4a .opus .webm

{ "success": true, "files": [
  { "name": "utwor.mp3", "size": 5242880,
    "relativePath": "media/utwor.mp3",
    "fullPath": "/opt/scscript/stations/station_radio1/media/utwor.mp3" }
]}
fullPath wstawiasz wprost do playlist, eventów, kampanii reklamowych i padów cart wall.

Limit wielkości: MAX_FILE_SIZE_MB (domyślnie 300).

DELETE/stations/:id/filesusunięcie pliku

?type=media&name=x.mp3

GET/stations/:id/files/downloadpobranie pliku

?name=x.mp3

PUT/stations/:id/files/playlist/:namezapis playlisty z poziomu menedżera plików
05Biblioteka muzyczna
POST/stations/:id/library/scanuruchomienie skanu
{ "deep": true, "force": false }

Skan deep analizuje dźwięk jednym przebiegiem ffmpeg i zapisuje:

PoleCo to
cue_start / cue_endpunkty przycięcia ciszy na końcach utworu
mix_pointmoment rozpoczęcia crossfade
gain_dbkorekta do −16 LUFS — koniec skakania głośności

Silnik używa tych danych automatycznie przy odtwarzaniu.

GET/stations/:id/library/scanpostęp skanu
{ "success": true, "scan": { "running": true, "done": 142, "total": 380, "errors": 0 } }
GET/stations/:id/libraryprzeglądanie biblioteki

Parametry: ?q= (szukajka) &limit=&offset=

{ "success": true, "total": 380, "rows": [ { "file": "…", "artist": "…", "title": "…", "gain_db": -2.3, … } ] }
GET/stations/:id/library/trackdane jednego utworu

?file=/pełna/ścieżka.mp3

PUT/stations/:id/library/trackedycja utworu
{ "file": "/…/utwor.mp3",
  "artist": "…", "title": "…", "album": "…", "year": 2019,
  "genre": "pop", "rating": 4, "tags": "hit,lato",
  "cue_start": 1.9, "cue_end": 210.4, "mix_point": 205.0, "gain_db": -2.3,
  "enabled": true,
  "daypart_from": 6, "daypart_to": 22,
  "date_from": "2026-12-01", "date_to": "2026-12-31" }
PoleDziałanie
rating0–5; rotacje mogą filtrować ratingMin
daypart_from/toutwór grany tylko w tych godzinach
date_from/tonp. utwory świąteczne tylko w grudniu
enabledfalse = wyłączony z rotacji bez kasowania pliku
GET/stations/:id/reportsraport odtworzeń (ZAiKS / OZZ)

Parametry: ?from=2026-07-01&to=2026-07-31&type=list|playcount&format=json|csv

type=list — każde odtworzenie z godziną. playcount — zliczenia per utwór. CSV otwiera się w Excelu z polskimi znakami.

06DJ-e
GET/stations/:id/djslista kont DJ
{ "success": true, "djs": [ { "name": "maciek", "priority": 5, "enabled": true } ] }
POST/stations/:id/djsnowe konto DJ
{ "name": "maciek", "password": "tajne123", "priority": 5, "enabled": true }
Działa natychmiast, bez restartu stacji. Konta czytane są przy każdym połączeniu — dodany DJ może wejść sekundę później, usunięty jest odrzucany od razu.
Jak łączy się DJ (BUTT / Mixxx)
UstawienieWartość
Typ serweraSHOUTcast
Adreshost agenta
Portport stacji + 2 (stacja 8000 → 8002)
Hasłonazwa:hasło — z dwukropkiem

Wyższy priority wypycha niższego z anteny.

PUT/stations/:id/djs/:namezmiana hasła, priorytetu lub aktywności
{ "password": "…", "priority": 9, "enabled": false }
DELETE/stations/:id/djs/:nameusunięcie konta
POST/stations/:id/djs/kickzrzucenie DJ-a z anteny

Autopilot wraca natychmiast.

07Głos na antenie

Wszystko poniżej gra na muzyce (overlay z duckingiem), nie zamiast niej.

POST/stations/:id/autopilot/announcezapowiedź TTS
{ "text": "Słuchasz Radia Przykład", "duck": 0.25, "gain": 1.0 }

duck — głośność podkładu pod głosem (0–1). Silnik: piper (lepszy głos, wymaga PIPER_MODEL) z fallbackiem na espeak-ng.

Jednocześnie może grać tylko jedna zapowiedź — kolejna dostaje 400.

POST/stations/:id/autopilot/saytimepodanie godziny
{ "duck": 0.25 }
{ "success": true, "text": "Jest godzina dwudziesta pierwsza trzydzieści" }

Poprawna polska odmiana godzin.

POST/stations/:id/autopilot/weatherzapowiedź pogody
{ "temp": 21, "desc": "słonecznie", "duck": 0.3 }

Odmienia stopnie: stopień / stopnie / stopni.

GET/stations/:id/autopilot/cartskonfiguracja cart wall
PUT/stations/:id/autopilot/cartszapis padów
{ "carts": [
  { "label": "Dżingiel", "file": "/…/jingiel.mp3", "duck": 0.3, "gain": 1.0 }
]}

Maksymalnie 12 padów. W panelu pod klawiszami 1–9.

POST/stations/:id/autopilot/cartzagranie pada natychmiast
{ "file": "/…/jingiel.mp3", "duck": 0.3 }
08Reklamy
GET/stations/:id/adskonfiguracja reklam i lista kampanii
PUT/stations/:id/adsbloki i elementy stałe
{ "blocks": ["08:00", "12:00", "16:30"],
  "intro": "/…/ads/intro.mp3",
  "outro": "/…/ads/outro.mp3",
  "separator": "/…/ads/sep.mp3",
  "maxBlockSec": 180 }
POST/stations/:id/ads/campaignsnowa kampania
{ "name": "Promocja lato", "client": "Pizzeria Roma",
  "file": "/…/ads/spot.mp3",
  "type": "gastronomia",
  "priority": 3,
  "dateFrom": "2026-07-01", "dateTo": "2026-07-31",
  "enabled": true,
  "grid": { "08:00": 1, "12:00": 2 } }
PoleDziałanie
priority1 = początek bloku … 9 = koniec
typespoty tego samego typu nie lecą pod rząd
dateFrom/Tookres emisji — poza nim kampania milczy
gridile emisji w którym bloku
PUT/stations/:id/ads/campaigns/:cidedycja kampanii
DELETE/stations/:id/ads/campaigns/:cidusunięcie kampanii
PUT/stations/:id/ads/gridprzypisanie kampanii do bloku
{ "id": "a3f9…", "blockTime": "12:00", "count": 2 }

count: 0 usuwa z bloku.

GET/stations/:id/ads/previewmedia plan na dziś
{ "success": true, "plan": [ { "blockTime": "12:00", "spots": 3, "items": 5, "warnings": [] } ] }
GET/stations/:id/ads/validatekontrola konfiguracji

Wykrywa: brakujące pliki, wygasłe kampanie, kampanie nieprzypisane do żadnego bloku, dwa spoty tego samego typu pod rząd.

POST/stations/:id/ads/testwypuszczenie bloku natychmiast
{ "blockTime": "12:00" }
GET/stations/:id/ads/reportraport emisji — dowód dla reklamodawcy

Parametry: ?from=&to=&groupBy=campaign|list&campaignId=&format=json|csv

groupBy=campaign — podsumowanie: ile emisji, łączny czas, pierwsza i ostatnia.

groupBy=list&format=csvdowód emisji: data, godzina, klient, kampania, blok, długość każdej emisji.

09Życzenia słuchaczy
GET/stations/:id/requests/config-publicczy życzenia są włączone bez tokenu
{ "success": true, "enabled": true }
GET/stations/:id/requests/searchwyszukiwanie utworu bez tokenu

?q=zima — min. 2 znaki

{ "success": true, "results": [
  { "file": "/…/zima.mp3", "artist": "Tomek", "title": "Zima", "duration": 214 }
]}

Zwraca tylko utwory enabled, maksymalnie 15.

POST/stations/:id/requestszgłoszenie życzenia bez tokenu
{ "file": "/…/zima.mp3", "name": "Ania", "message": "Pozdro dla ekipy" }
{ "success": true, "status": "pending" }
{ "success": true, "status": "queued" }   ← przy autoApprove

Anty-spam: limit zgłoszeń na IP na godzinę + blokada duplikatów w kolejce.

GET/stations/:id/requestslista oczekujących (moderacja)
{ "success": true, "pending": [ { "id": "…", "name": "Ania", "file": "…", "message": "…" } ], "stats": { … } }
PUT/stations/:id/requests/configustawienia życzeń
{ "enabled": true, "autoApprove": false, "maxPerIpPerHour": 3, "playNext": false }
POST/stations/:id/requests/:rid/approveakceptacja → do kolejki anteny
POST/stations/:id/requests/:rid/rejectodrzucenie życzenia
10Wyjścia: SHOUTcast, Icecast, RTMP

Jedna stacja może nadawać w kilku formatach naraz — pole autopilotConfig.outputs:

"outputs": [
  { "format": "mp3", "bitrate": 128, "streamid": 1 },

  { "type": "icecast", "format": "opus", "bitrate": 96,
    "mount": "/radio.opus", "host": "127.0.0.1", "port": 8000,
    "username": "source", "password": "hasło" },

  { "type": "rtmp", "format": "aac", "bitrate": 128,
    "url": "rtmp://a.rtmp.youtube.com/live2/KLUCZ",
    "image": "/…/logo.png" }
]
TypKodekiUwagi
shoutcast
domyślny
mp3, aacstreamid = numer streamu w DNAS
icecastmp3, aac, ogg, opus, flacwymaga mount i hasła source
rtmpaac + obraz H.264YouTube Live, Twitch
Uwagi techniczne: ogg/opus/flac działają tylko z Icecastem — DNAS ich nie przyjmie, agent sam wymusza wtedy type: icecast. opus wymaga 48 kHz, enkoder resampluje automatycznie. flac jest bezstratny, bitrate nie ma zastosowania. RTMP restartuje się sam po zerwaniu.
11Now playing i archiwum
GET/stations/:id/nowplayingco gra teraz bez tokenu
{ "success": true, "nowPlaying": { "currentSong": "Artysta — Tytuł", "listeners": 42, "online": true } }
GET/stations/:id/nowplaying/historyhistoria odtworzeń bez tokenu

?limit=20

GET/stations/:id/nowplaying/streamstrumień zdarzeń (SSE) bez tokenu

Server-Sent Events — do widgetu aktualizowanego na żywo.

GET/stations/:id/nowplaying/listen.plsplaylista dla odtwarzaczy bez tokenu
GET/stations/:id/nowplaying/mediaokładka, tekst, teledysk bez tokenu
{ "success": true, "media": {
  "track": { "artist": "…", "title": "…" },
  "cover": "https://… albo /stations/…/nowplaying/cover?f=…",
  "lyrics": { "plain": "…", "synced": "[00:12.30]linia…", "source": "lrclib" },
  "video": { "url": "https://…preview.m4v", "poster": "https://…" }
}}

Źródła po kolei: grafika i tekst osadzone w pliku → iTunes Search API (okładka 600×600, oficjalny 30-sekundowy teledysk) → LRCLIB (tekst, także zsynchronizowany). Wyniki cachowane 30 dni per plik.

GET/stations/:id/nowplaying/coverokładka z cache bez tokenu

?f=nazwa.mp3 → JPEG

GET/stations/:id/archivelista nagrań anteny
{ "success": true, "files": [
  { "name": "2026-07-23_14.mp3", "size": 57802240, "mtime": 1753280000000 }
]}

Włączenie: autopilotConfig.archive = { "enabled": true, "keepDays": 14 }. Pliki godzinowe, starsze niż keepDays kasują się same.

GET/stations/:id/archive/:namepobranie nagrania
DELETE/stations/:id/archive/:nameusunięcie nagrania
12Ustawienia agenta — proxy SSL i połączenia

Ustawienia całego serwera, nie stacji. Wymagają tokenu master. Zapisywane w agent-settings.json — wartość z panelu wygrywa nad .env.

GET/settingsodczyt ustawień agenta
{ "success": true, "settings": {
  "proxy": {
    "publicPanelUrl": "https://radio.domena.pl",
    "publicStreamBase": "https://radio.domena.pl/s"
  },
  "webhook": {
    "enabled": true,
    "url": "https://scscript.pl/api/agent-events",
    "secret": "••••••••",
    "events": { "adPlayed": true, "djOnOff": true, "stationDown": true,
                "nowPlaying": false, "requestNew": false },
    "lastOk": "2026-07-23T12:00:03Z", "lastError": null
  }
}}

Sekret nigdy nie wraca w jawnej postaci — tylko informacja, że jest ustawiony.

PUT/settingszapis ustawień
{ "proxy": { "publicPanelUrl": "…", "publicStreamBase": "…" },
  "webhook": { "enabled": true, "url": "…", "secret": "…",
               "events": { "adPlayed": true, … } } }

Pola opcjonalne — zapisujesz tylko to, co zmieniasz. Pominięty albo zamaskowany (••••••••) sekret zostawia istniejący bez zmian.

publicStreamBase trafia natychmiast do GET /stations/:id/status — panel i apka zaczynają budować linki https zamiast http://host:port.

POST/settings/webhook/testtestowe uderzenie do SCScripta
{ "success": true, "status": 200, "ms": 33 }
{ "success": false, "error": "HTTP 404 Not Found" }

Wysyła pakiet event: "test" z pełnym podpisem — dobre do sprawdzenia, czy SCScript poprawnie weryfikuje HMAC.

GET/settings/ssl/checkdiagnostyka HTTPS
{ "success": true, "ok": false,
  "checks": [
    { "name": "Panel po HTTPS", "ok": false, "detail": "połączenie po http",
      "hint": "Bez HTTPS nie zadziała mikrofon w apce mobilnej…" },
    { "name": "Za reverse proxy", "ok": false, "detail": "brak nagłówków proxy",
      "hint": "Dodaj proxy_set_header X-Forwarded-Proto $scheme;" },
    { "name": "Publiczny adres streamów", "ok": true, "detail": "https://radio.domena.pl/s" },
    { "name": "Certyfikat Let's Encrypt", "ok": false, "detail": "brak w /etc/letsencrypt/live" }
  ],
  "command": "sudo bash deploy/setup-ssl.sh radio.domena.pl twoj@email.pl" }
Agent nie stawia certyfikatu sam. Certbot wymaga uprawnień roota i zmienia konfigurację systemu — panel podaje gotową komendę, ale uruchamiasz ją świadomie na serwerze. Skrypt konfiguruje nginx (panel, streamy przez /s/PORT/, WebSocket), pobiera certyfikat i sam uzupełnia publicStreamBase.

Wykrywa też niespójność: panel po HTTPS + streamy po HTTP = przeglądarka zablokuje odtwarzanie (mixed content).

Pakiety wysyłane do SCScripta

Każdy jako POST z nagłówkiem podpisu:

X-SCScript-Signature: HMAC-SHA256(secret, surowe-ciało-żądania)

Po stronie SCScripta policz to samo i porównaj — to odrzuca podszywanie się.

{ "event": "adPlayed", "agentId": "radio-srv-1",
  "at": "2026-07-23T12:00:03.412Z", "data": { … } }
eventdata zawieraDo czego w SCScripcie
adPlayedstationId, campaignId, campaignName, client, blockTime, file, duration, playedAtrozliczenia z reklamodawcami — dowód każdej emisji
djOnOffstationId, dj, statestatystyki prowadzących, podgląd kto na antenie
stationDownstationId, whatmonitoring, SLA, alerty
nowPlayingstationId, artist, title, listenerswidget na stronie — uwaga, ~17 pakietów/h na stację
requestNewstationId, file, name, messagemoderacja życzeń z poziomu SCScripta
Agent czeka na odpowiedź maksymalnie 6 s i nigdy nie blokuje anteny — błąd webhooka trafia do logu i pola lastError widocznego w panelu.
13Serwer nadawania i system
GET/stations/:id/shoutcast/statusstatus DNAS: słuchacze, bitrate, uptime
GET/stations/:id/shoutcast/listenerslista podłączonych słuchaczy
POST/stations/:id/shoutcast/kicksrcrozłączenie źródła
POST/stations/:id/shoutcast/kickdstrozłączenie słuchacza
GET/healthstan agenta
{ "success": true, "version": "3.12.0", "uptime": 84213 }

bez tokenu

GET/cluster/statusstan węzłów (praca wielowęzłowa)

WebSocket — nadawanie na żywo

ws(s)://HOST/live/:stationId?token=<token>&name=Studio
  1. Połącz się z tokenem stacji albo master
  2. Wysyłaj binarne pakiety audio/webm;codecs=opus (np. MediaRecorder co 250 ms)
  3. Serwer odpowiada {"type":"onair"} gdy wejdziesz na antenę
  4. Zamknięcie socketu = zejście, autopilot wraca

Obowiązuje ten sam djMode co dla DJ-ów — domyślnie ducking.

Mikrofon wymaga HTTPS. Przeglądarki udostępniają go wyłącznie po szyfrowanym połączeniu (poza localhost). Do nadawania z telefonu postaw SSL przez deploy/setup-ssl.sh.

Pełny cykl — od zera do nadawania

A=http://serwer:3500
H="Authorization: Bearer $MASTER_TOKEN"
CT="Content-Type: application/json"

# 1. Stacja (hasła MUSZĄ się różnić)
curl -X POST $A/stations -H "$H" -H "$CT" -d '{
  "stationId": "radio1",
  "scServConfig": { "port": 8000, "adminPassword": "adm-1",
                    "sourcePassword": "src-2", "streamTitle": "Radio Przykład",
                    "maxListeners": 100 } }'
# → zapisz zwrócony token

# 2. Muzyka
curl -X POST "$A/stations/radio1/files?type=media" -H "$H" \
     -F "files=@utwor1.mp3" -F "files=@utwor2.mp3"

# 3. Playlista (fullPath z odpowiedzi wyżej)
curl -X PUT $A/stations/radio1/playlists/main -H "$H" -H "$CT" -d '{
  "tracks": ["/opt/scscript/stations/station_radio1/media/utwor1.mp3",
             "/opt/scscript/stations/station_radio1/media/utwor2.mp3"] }'

# 4. Ramówka — dżingiel co godzinę
curl -X PUT $A/stations/radio1/autopilot/schedule -H "$H" -H "$CT" -d '{
  "defaultPlaylist": "main",
  "events": [{ "type": "spot", "name": "Dżingiel", "hourly": true, "minute": 0,
               "file": "/opt/scscript/stations/station_radio1/media/utwor1.mp3",
               "mode": "queue" }],
  "rotations": {} }'

# 5. Start
curl -X POST $A/stations/radio1/start -H "$H"

# 6. Kontrola — sprawdź connected i bytesOut
curl $A/stations/radio1/autopilot/status -H "$H"

# Stream dla słuchaczy: http://serwer:8000/;

Zmienne środowiskowe

ZmiennaDomyślnieOpis
AGENT_PORT3500port panelu i API
AGENT_MASTER_TOKENtoken pełnego dostępu
AGENT_SECRETklucz HMAC do tokenów stacji i ról
STATIONS_BASE_DIR/opt/scscript/stationskatalog danych stacji
BINARIES_DIRbingdzie leży sc_serv
PUBLIC_STREAM_BASEnp. https://domena/s — ustawia setup-ssl.sh
MAX_FILE_SIZE_MB300limit uploadu
LOG_LEVELinfoerror / warn / info / debug
WATCHDOG_INTERVAL10000ms między kontrolami procesów
NOW_PLAYING_INTERVAL5000ms między odpytaniami DNAS
PIPER_BIN / PIPER_MODELlepszy głos TTS
YTDLP_BINyt-dlpścieżka do yt-dlp
ITUNES_BASE / LRCLIB_BASEoficjalnepodmiana źródeł okładek i tekstów
ALLOWED_ORIGINSCORS dla zewnętrznych panelów
Kolejność montowania ma znaczenie. Trasy publiczne (/nowplaying, /requests) są rejestrowane przed chronionymi — inaczej middleware autoryzacji odrzuciłby żądanie zanim dotrze do handlera.
07 — Integracja

SCScript nad flotą agentów

Agent jest w pełni sterowalny przez REST — panel www to tylko klient tego samego API. Panel nadrzędny trzyma klientów i płatności, a operacje techniczne wykonuje żądaniami do agentów.

Zdarzenie w SCScriptWywołanie do agenta
Klient kupuje stacjęPOST /stations → zapisz zwrócony token przy koncie klienta
Panel klientaTen sam agent, ale token owner (dostęp tylko do jego stacji)
Klient dodaje DJ-aPOST /stations/:id/djs — skutek natychmiastowy
Brak płatnościPOST /stations/:id/stop
WznowieniePOST /stations/:id/start
RezygnacjaDELETE /stations/:id
Monitoring flotyGET /stations + status każdej stacji cyklicznie
Bezpieczeństwo: token master nigdy nie trafia do przeglądarki klienta. Backend SCScript woła agenta, klient rozmawia tylko z SCScript. Ruch między nimi po HTTPS albo w sieci prywatnej.
08 — Wdrożenie

Uruchomienie na serwerze

Linux (produkcja)

sudo bash deploy/install.sh
# Node 22 z NodeSource (repo Debiana daje 20 — za stare dla node:sqlite),
# ffmpeg, espeak-ng, usługa systemd startująca po restarcie serwera

sudo bash deploy/setup-ssl.sh radio.domena.pl admin@domena.pl
# nginx + Let's Encrypt:
#   https://domena/          panel
#   https://domena/s/8000/;  stream stacji
#   https://domena/app/      apka PWA (mikrofon wymaga HTTPS)

Windows (testy, mniejsze instalacje)

deploy\install-windows.bat   :: ffmpeg, tokeny, sc_serv.exe wg architektury
deploy\start-windows.bat     :: doinstalowuje npm i uruchamia agenta

Wymagania

Node 22+

node:sqlite istnieje dopiero od 22. Instalator wymusza właściwą wersję.

ffmpeg

Dekodowanie, enkodery, analiza głośności, archiwum, RTMP.

espeak-ng

Zapowiedzi TTS. Opcjonalnie piper z modelem polskim dla lepszego głosu.

yt-dlp

Opcjonalnie — nadawanie z linków YouTube.

Icecast

Opcjonalnie — jeśli chcesz wyjścia OGG/Opus/FLAC.

Porty

3500 (panel) + port każdej stacji i port+2 dla DJ-ów.

09 — Eksploatacja

Pułapki, które już kosztowały czas

Każda z tych rzeczy zatrzymała system w praktyce i została naprawiona u źródła. Zapisane, żeby nie wróciły.

ObjawPrzyczyna i naprawa
sc_serv nie startuje, w logu You must specify different passwords DNAS odmawia startu, gdy adminpassword = password. Panel blokuje to przy zakładaniu, a generator poprawia automatycznie.
Invalid item ... streamtitle W DNAS 2.6 nie ma pól streamtitle/genre/description — metadane idą nagłówkami ICY od źródła. Usunięte z generatora.
Port zajęty po restarcie agenta (Windows) sc_serv.exe nie ginie razem z rodzicem. Agent przy starcie znajduje sieroty po PID-ach i ubija (taskkill /T).
npm install pada na Visual Studio better-sqlite3 wymagał kompilacji. Zastąpiony wbudowanym node:sqlite — zero zależności natywnych.
Cisza mimo działającego serwera Pusta playlista. Panel pokazuje wprost „PLAYLISTA PUSTA" zamiast myślnika.
ECONNREFUSED w logu przy starcie Autopilot wstawał przed sc_serv. Kolejność odwrócona, z czekaniem na gotowość.
Wejście DJ-a przerywało utwór Było switchTo + playNext po zejściu. Jest ducking: muzyka gra pod głosem i wraca ta sama.
Opus nie łączył się z Icecastem libopus przyjmuje tylko 48 kHz. Enkoder wymusza resampling.
Widget „teraz gramy" zwracał 401 Trasy publiczne montowane po chronionych. Kolejność poprawiona.

Diagnostyka w panelu

Przycisk 🔍 Diagnoza na zakładce Antena uruchamia checklistę: binarka, prawa do pliku, konfiguracja, port, osierocone procesy, zacięte blokady startu. Każdy punkt ma podpowiedź naprawy. Zakładka Baza wiedzy zawiera przeszukiwalne artykuły dla operatora.