Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
gAlleb committed Dec 24, 2024
0 parents commit 40f18b2
Show file tree
Hide file tree
Showing 37 changed files with 3,330 additions and 0 deletions.
Binary file added .DS_Store
Binary file not shown.
3 changes: 3 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# .dockerignore
.gitignore
README.md
206 changes: 206 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
### So this is very very basic hobby radio setup for folks who're trying out `liquidsoap` and `icecast` and want to have `nowplaying` info on their websites or try at local home enviroment. All you need is Docker.

### It illustrates:

- How Liquidsoap can send `nowplaying` info to your website or homepage using `Server Sent Events` or `Websocket`.
- It urges you to stop fetching our `tired` `status-json.xsl` :) Because you may want to stop using `Icecast` at all and look into `HLS`.
- Liquidsoap doesn't send data directly to `Centrifugo` (although it can) because it's more fancy to have an API where data is collected and sent further enabling "on_track_change" notifications and tunable intervals.

### What we have inside:

- Liquidsoap v2.3.0 - https://github.com/savonet/liquidsoap (With `entrypoint` script from @vitoucepi)
- Icecast KH icecast-2.4.0-kh22 - https://github.com/karlheyes/icecast-kh
- Centrifugo latest - https://github.com/centrifugal/centrifugo
- Node.js API

### To setup

- Download the repo
- Change `- /path/to/music:/home/radio/music` in the `docker-compose.yaml` file. `/path/to/music` -> `/your/real/path/to/music`
- RUN: `docker compose up -d`
- It's nice to run under 1000 user acoount.

### Of course you can edit and tune everything inside `docker folder` and scale it to be a real radio station on the internet.

<hr/>

## After installation you'll have following endpoints:

- http://localhost:8000/stream - your Icecast stream with music
- http://localhost:8007/skipQueue?queueType=default - skips default queue
- http://localhost:8007/skipQueue - skips interrupting queue
- http://localhost:8007/queuePlaylist?track_sensitive=true&playlist_name=/path/to/playlist - queues playlist to default queue
- http://localhost:8007/queuePlaylist?playlist_name=/path/to/playlist - queues playlist to interrupting queue
- http://localhost:8007/queueFile?track_sensitive=true&file=/path/to/file - queues file to default queue
- http://localhost:8007/queueFile?file=/path/to/file - queues file to interrupting queue
- http://localhost:8007/metadata - all metadata endpoint
- http://localhost:8007/skip - skip endpoint
- http://localhost:8007/nowplaying - nowplaying info endpoint
Will give us a json with nowplaying and song history:

```
{"station":
{"name":"Radio","shortcode":"radio","timezone":"Europe/London"},
"now_playing":
{"played_at":1735060884.4,
"played_at_timestamp_old":"1735060884.39",
"played_at_date_time_old":"2024/12/24 17:21:24",
"duration":250.494943311,
"elapsed":57.38,
"remaining":193.114943311,
"playlist":"",
"filename":"/home/radio/music/Various Artists - BIRP! Best of 2020/041 - Kowloon - Come Over.mp3",
"song":
{"text":"Kowloon - Come Over",
"artist":"Kowloon",
"title":"Come Over",
"album":"Come Over - Single",
"genre":"Alternative"}},
"song_history":
[{"played_at":1735060884.4,"played_at_timestamp_old":"1735060884.39","played_at_timestamp":1735060884.4,"played_at_date_time_old":"2024/12/24 17:21:24","played_at_date_time":"2024/12/24 17:21:24","playlist":"","filename":"/home/radio/music/Various Artists - BIRP! Best of 2020/041 - Kowloon - Come Over.mp3","song":{"text":"Kowloon - Come Over","artist":"Kowloon","title":"Come Over","album":"Come Over - Single","genre":"Alternative"}}]}
```

Don't fetch it :) cause we got SSE and WS :) :

### Endpoint to get SSE (WS) events

- wss://localhost:9998/connection/ws
- http://localhost:9998/connection/sse

To get our station data we connect to:

- http://localhost:9998/connection/sse?cf_connect={%22subs%22:{%22station:radio%22:{%22recover%22:true}}}

Basic `js` implementations (Just like `AzuraCast` ones):

```
const sseBaseUri = "http://localhost:9998/connection/sse";
const sseUriParams = new URLSearchParams({
"cf_connect": JSON.stringify({
"subs": {
"station:radio": {"recover": true}
}
})
});
const sseUri = sseBaseUri+"?"+sseUriParams.toString();
const sse = new EventSource(sseUri);
let nowplaying = {};
let currentTime = 0;
// This is a now-playing event from a station. Update your now-playing data accordingly.
function handleSseData(ssePayload, useTime = true) {
const jsonData = ssePayload.data;
if (useTime && 'current_time' in jsonData) {
currentTime = jsonData.current_time;
}
nowplaying = jsonData.np;
}
sse.onmessage = (e) => {
const jsonData = JSON.parse(e.data);
if ('connect' in jsonData) {
const connectData = jsonData.connect;
if ('data' in connectData) {
// Legacy SSE data
connectData.data.forEach(
(initialRow) => handleSseData(initialRow)
);
} else {
// New Centrifugo time format
if ('time' in connectData) {
currentTime = Math.floor(connectData.time / 1000);
}
// New Centrifugo cached NowPlaying initial push.
for (const subName in connectData.subs) {
const sub = connectData.subs[subName];
if ('publications' in sub && sub.publications.length > 0) {
sub.publications.forEach((initialRow) => handleSseData(initialRow, false));
}
}
}
} else if ('pub' in jsonData) {
handleSseData(jsonData.pub);
}
};
```

WS:

```
let socket = new WebSocket("wss://localhost:9998/connection/ws);
socket.onopen = function(e) {
socket.send(JSON.stringify({
"subs": {
"station:radio": {"recover": true}
}
});
};
let nowplaying = {};
let currentTime = 0;
// Handle a now-playing event from a station. Update your now-playing data accordingly.
function handleSseData(ssePayload, useTime = true) {
const jsonData = ssePayload.data;
if (useTime && 'current_time' in jsonData) {
currentTime = jsonData.current_time;
}
nowplaying = jsonData.np;
}
socket.onmessage = function(e) {
const jsonData = JSON.parse(e.data);
if ('connect' in jsonData) {
const connectData = jsonData.connect;
if ('data' in connectData) {
// Legacy SSE data
connectData.data.forEach(
(initialRow) => handleSseData(initialRow)
);
} else {
// New Centrifugo time format
if ('time' in connectData) {
currentTime = Math.floor(connectData.time / 1000);
}
// New Centrifugo cached NowPlaying initial push.
for (const subName in connectData.subs) {
const sub = connectData.subs[subName];
if ('publications' in sub && sub.publications.length > 0) {
sub.publications.forEach((initialRow) => handleSseData(initialRow, false));
}
}
}
} else if ('pub' in jsonData) {
handleSseData(jsonData.pub);
}
};
```

Example of incoming data:

```
data: {"connect":
{"client":"4fe1-9386-2ef1ede3f0fa","version":"5.4.9",
"subs":{"station:radio":
{"recoverable":true,"epoch":"kCIB",
"publications":
[{"data":{"np":{"station":
{"name":"Station","shortcode":"radio","timezone":"Europe/London"},
"now_playing":
{"played_at":1735060884.4,"played_at_timestamp_old":"1735060884.39","played_at_timestamp":1735060884.4,"played_at_date_time_old":"2024/12/24 17:21:24","played_at_date_time":"2024/12/24 17:21:24","duration":250.494943311,"elapsed":12.66,"remaining":237.834943311,"playlist":"","filename":"/home/radio/music/Various Artists - BIRP! Best of 2020/041 - Kowloon - Come Over.mp3","song":{"text":"Kowloon - Come Over","artist":"Kowloon","title":"Come Over","album":"Come Over - Single","genre":"Alternative"}},
"song_history":
[{"played_at":1735060884.4,"played_at_timestamp_old":"1735060884.39","played_at_timestamp":1735060884.4,"played_at_date_time_old":"2024/12/24 17:21:24","played_at_date_time":"2024/12/24 17:21:24","playlist":"","filename":"/home/radio/music/Various Artists - BIRP! Best of 2020/041 - Kowloon - Come Over.mp3","song":{"text":"Kowloon - Come Over","artist":"Kowloon","title":"Come Over","album":"Come Over - Single","genre":"Alternative"}}]}},"offset":3}],"recovered":true,"positioned":true,"was_recovering":true}},"ping":25,"session":"043b7d46-ab47","time":1735060899380}}
```
104 changes: 104 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
services:
nodeapi:
container_name: nodeapi
build:
context: ./
dockerfile: ./docker/nodeapi/Dockerfile
depends_on:
- centrifugo
environment:
- "TZ=Europe/London"
user: node:node
restart: always
ports:
- 9999:9999
networks:
- radio-network
logging:
driver: "json-file"
options:
max-size: "1m"
max-file: "3"

centrifugo:
container_name: centrifugo
restart: always
image: centrifugo/centrifugo:latest
volumes:
- ./docker/centrifugo/config.json:/centrifugo/config.json
command: centrifugo -c config.json
ports:
- 9998:9998
networks:
- radio-network
depends_on:
- icecast
ulimits:
nofile:
soft: 65535
hard: 65535
logging:
driver: "json-file"
options:
max-size: "1m"
max-file: "3"

liquidsoap:
build:
context: ./
dockerfile: ./docker/liquidsoap/Dockerfile
args:
- "USER_UID=${USER_UID:-1000}"
- "USER_GID=${USER_GID:-1000}"
user: 1000:1000
container_name: liquidsoap-2.3.0
environment:
- "TZ=Europe/London"
command:
- /home/radio/liquidsoap/radio/index.liq
volumes:
#- /path/to/web/folder/for/hls:/home/radio/liquidsoap/radio/hls
- /path/to/music:/home/radio/music
- ./docker/liquidsoap/rootfs/home/radio/liquidsoap/radio/log:/home/radio/liquidsoap/radio/log
networks:
- radio-network
depends_on:
- nodeapi
ports:
- 8007:8007
restart: always
logging:
driver: "json-file"
options:
max-size: "1m"
max-file: "3"

icecast:
build:
context: ./
dockerfile: ./docker/icecast/Dockerfile
args:
- VERSION=kh-icecast-2.4.0-kh22
container_name: icecast
restart: always
networks:
- radio-network
ports:
- 8000:8000
environment:
- "TZ=Europe/London"
- ICECAST_SOURCE_PASSWORD=hackme
- ICECAST_ADMIN_PASSWORD=hackme
- ICECAST_RELAY_PASSWORD=hackme
- ICECAST_HOSTNAME=localhost
logging:
driver: "json-file"
options:
max-size: "1m"
max-file: "3"

networks:
radio-network:
driver: bridge


Binary file added docker/.DS_Store
Binary file not shown.
Binary file added docker/centrifugo/.DS_Store
Binary file not shown.
38 changes: 38 additions & 0 deletions docker/centrifugo/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"port": 9998,
"admin_password": "111fff50-1111-1111-841a80111fff",
"admin_secret": "111fff9a-1111-1111-1111-8cf7ba111fff",
"api_insecure": false,
"api_key": "111fff39-283d-1111-88ea-111fff98a98a",
"allowed_origins": [
"*"
],
"admin": true,
"uni_sse": true,
"websocket_disable": true,
"uni_websocket": true,
"uni_http_stream": true,
"internal_port": 9998,
"allow_subscribe_for_client": true,
"allow_anonymous_connect_without_token": true,
"admin_insecure": false,
"swagger": false,
"namespaces": [
{
"name": "station",
"history_size": 1,
"history_ttl": "30s",
"allow_subscribe_for_client": true,
"allow_subscribe_for_anonymous": true,
"allow_history_for_client": true,
"allow_history_for_anonymous": true,
"force_recovery": true,
"force_recovery_mode": "cache"
}
],
"uni_sse_handler_prefix": "/connection/sse",
"uni_websocket_handler_prefix": "/connection/ws",
"client_connect_include_server_time": true
}


Binary file added docker/icecast/.DS_Store
Binary file not shown.
Loading

0 comments on commit 40f18b2

Please sign in to comment.