# Mekkify

Mekkify is an Internet radio application that reads FLAC files from a local directory, converts them to Ogg Vorbis with FFmpeg, and publishes the result to an Icecast2 mountpoint. A PHP frontend calls the Node.js API to control playback and reads Icecast metadata for the player display.

## Requirements

- Node.js with npm
- FFmpeg and FFprobe available at `/usr/bin/ffmpeg` and `/usr/bin/ffprobe`
- Icecast2 with a configured source account
- PHP with a web server for the frontend
- A directory containing FLAC files
- Network access between the Mekkify host and the Icecast server

## Installation

1. Install the Node.js dependencies:

   ```sh
   npm install
   ```

2. Copy the example configuration:

   ```sh
   cp config-example.json config.json
   ```

3. Edit `config.json` with the values for your system.

4. Make sure the PHP document root exposes `index.php`, `frontend-methods.php`, `style.css`, and the cover image files.

5. Start the Node.js API and Icecast source process:

   ```sh
   node main.js
   ```

   The included `start.sh` starts the process in a detached tmux session named `Mekkify`:

   ```sh
   ./start.sh
   ```

## Configuring config.json

The following is the structure of `config.json`:

```json
{
  "icecastserver": {
    "ICECAST_HOST": "icecast.example.com",
    "ICECAST_PORT": 8000,
    "ICECAST_USER": "source",
    "ICECAST_PASSWORD": "replace-with-source-password",
    "ICECAST_MOUNT": "/flacs.ogg"
  },
  "givenPath": "/absolute/path/to/flac/files",
  "apiport": 7887,
  "apiBaseUrl": "https://radio.example.com/mekkify-api/",
  "icecastFrontend": {
    "STREAM_URL": "https://radio.example.com/icecast/",
    "MOUNT_POINT": "/flacs.ogg",
    "STATUS_URL": "https://radio.example.com/icecast/status-json.xsl"
  },
  "silenceArgs": [
    "-re",
    "-f",
    "lavfi",
    "-i",
    "anullsrc=r=44100:cl=stereo",
    "-c:a",
    "libvorbis",
    "-b:a",
    "128k",
    "-ar",
    "44100",
    "-ac",
    "2",
    "-content_type",
    "audio/ogg",
    "-f",
    "ogg"
  ]
}
```

### `icecastserver`

These values are used by FFmpeg when it connects to Icecast as a source client.

- `ICECAST_HOST`: Hostname or IP address of the Icecast2 server. Use a value reachable from the Mekkify Node.js process.
- `ICECAST_PORT`: Icecast source port, normally `8000`.
- `ICECAST_USER`: Icecast source username, normally `source`.
- `ICECAST_PASSWORD`: Password for the source user. Keep this private.
- `ICECAST_MOUNT`: The Icecast mountpoint to publish, such as `/flacs.ogg`.

The mountpoint must match the mount configured or allowed by Icecast. The source URL generated by Mekkify is equivalent to:

```text
icecast://ICECAST_USER:ICECAST_PASSWORD@ICECAST_HOST:ICECAST_PORT/flacs.ogg
```

The application constructs this URL automatically from `config.json`.

### `givenPath`

Absolute path containing the music files. Mekkify recursively scans this directory for files ending in `.flac` when it starts or when the refresh endpoint is called.

### `apiport`

TCP port used by the Node.js Express API. The PHP frontend or reverse proxy must be able to reach this port, directly or through a public API URL.

### `apiBaseUrl`

Base URL used by the PHP frontend for API requests. It should end with `/` because the frontend appends paths such as:

- `list`
- `stream/<song-id>`
- `stream/album/<album-name>`
- `queue/add/<song-id>`
- `list/queue`
- `icecast/status`

For a local installation, this may be `http://127.0.0.1:7887/`. For a public installation, use the URL exposed by your reverse proxy.

### `icecastFrontend`

These values describe how a browser reaches Icecast and its status endpoint.

- `STREAM_URL`: Public base URL for the Icecast HTTP listener. The frontend combines this with `MOUNT_POINT` to create the browser stream URL.
- `MOUNT_POINT`: Browser-facing mountpoint, normally `/flacs.ogg`. It should match `ICECAST_MOUNT`.
- `STATUS_URL`: Public Icecast JSON status endpoint, commonly `/status-json.xsl`.

The Node.js API fetches `STATUS_URL` and exposes it through `/icecast/status` so the PHP frontend can read metadata without depending on browser CORS permissions.

### `silenceArgs`

FFmpeg arguments used to publish silence to Icecast. Mekkify starts this process when the application starts and after a song or queue finishes. The final Icecast destination URL is added by the application, so do not add it to this array.

## Icecast2 setup

Configure Icecast2 so that:

1. The server listens on the port specified by `ICECAST_PORT`.
2. The source username and password match `ICECAST_USER` and `ICECAST_PASSWORD`.
3. The source is allowed to publish to `/flacs.ogg`.
4. The HTTP listener exposes the mountpoint at the public `STREAM_URL`.
5. The status endpoint is available at the configured `STATUS_URL`.

A typical direct Icecast stream URL is:

```text
https://radio.example.com/icecast/flacs.ogg
```

If a reverse proxy is used, it must forward the Icecast HTTP listener and preserve long-lived streaming connections. Do not buffer the stream in the proxy. The proxy should also expose `status-json.xsl` if the Node.js process is expected to fetch it through the public hostname.

The Icecast source account is a server credential. Do not expose it in browser JavaScript, PHP output, or public documentation.

## How the application connects to Icecast

The connection flow is:

1. `main.js` loads `config.json` and starts the Express API.
2. `methods.js` builds an Icecast source URL from the `icecastserver` values.
3. FFmpeg reads a FLAC file, converts it to Ogg Vorbis, adds Icecast metadata, and publishes it to the configured mountpoint.
4. Icecast serves the mountpoint to listeners.
5. The PHP frontend builds the public stream URL from `STREAM_URL` and `MOUNT_POINT`.
6. The browser audio player connects to that URL and polls the Node.js `/icecast/status` proxy for title, artist, bitrate, channel, and sample-rate metadata.
7. When a source process changes between tracks, the frontend retries the mountpoint connection after an `ended`, `error`, or `stalled` event.

At startup, Mekkify publishes the configured silent audio stream so the mountpoint is available before a song is selected. Selecting a song stops the silence process and starts a new FFmpeg source process for that file. When the file ends, the application returns to the silence process unless another playback flow starts.

## Useful API endpoints

The API listens on `apiport`:

- `GET /list`: Return the scanned song database.
- `GET /list/albums`: Return available albums and cover art URLs.
- `GET /stream/:id`: Start streaming one song.
- `GET /stream/album/:album`: Start an album sequence.
- `GET /refresh`: Rebuild the song database from `givenPath`.
- `GET /list/queue`: Return the queue.
- `GET /queue/add/:id`: Add a song to the queue.
- `GET /queue/true`: Start the queue.
- `GET /queue/false`: Stop playback and publish silence.
- `GET /drop/queue`: Clear the queue and publish silence.
- `GET /icecast/status`: Proxy the Icecast JSON status response.

## Troubleshooting

### The stream is unavailable

Check that:

- Icecast is running and reachable from the Mekkify host.
- `ICECAST_HOST`, `ICECAST_PORT`, `ICECAST_USER`, and `ICECAST_PASSWORD` are correct.
- `ICECAST_MOUNT` starts with `/` and matches the configured mountpoint.
- FFmpeg and FFprobe are installed at the paths used by the application.
- The Node.js process can read `givenPath` and its FLAC files.

### Metadata is not updating

Check that `STATUS_URL` returns valid Icecast JSON, then request the Node.js proxy directly:

```sh
curl http://127.0.0.1:7887/icecast/status
```

Also verify that `apiBaseUrl` points to the Node.js API and ends in `/`.

### Playback stops at a track change

The source connection may briefly disappear while FFmpeg is replaced. Confirm that the frontend can reach the public mountpoint and that a reverse proxy is not buffering or caching it. The frontend contains reconnect handling for the browser-side audio element, but the Icecast source process must still be able to authenticate and reconnect successfully.

## Security notes

- Do not commit `config.json` if it contains real Icecast credentials.
- Restrict access to the Node.js API or place it behind an authenticated reverse proxy.
- Use HTTPS for the public frontend, API, and stream when the page is served over HTTPS.
- Validate the Icecast source account permissions so it can publish only where intended.
