- Go 96.7%
- HTML 2.8%
- Dockerfile 0.5%
|
|
||
|---|---|---|
| cmd/grab | ||
| docs | ||
| lib | ||
| .gitignore | ||
| .golangci.yml | ||
| .woodpecker.yml | ||
| config.yaml.example | ||
| Dockerfile | ||
| Dockerfile.ci | ||
| go.mod | ||
| go.sum | ||
| LICENSE | ||
| README.md | ||
icecast-grab
Консольная утилита для записи Shoutcast/Icecast-аудиопотоков с извлечением ICY-метаданных (названия треков) и нарезкой по CUE-листу.
Use Cases
- Запись миксов и подкастов — split-режим раскладывает эфир на отдельные треки с ID3-тегами, готово к импорту в плеер.
- Ночные эфиры — single + CUE, утром
split-cueнарезает по трекам. Ничего не теряется, даже если название трека не передавалось. - Мониторинг эфира — HTTP-статус + JSONL-лог позволяют отслеживать что играло, когда и сколько.
- Архивирование — несколько станций параллельно, шаблон
{{.Date}}/{{.Artist}} - {{.Title}}.mp3раскладывает записи по датам. - Compliance — single-режим даёт оригинал записи с CUE-таймкодами. Фрагмент нельзя вырезать бесследно.
Ключевые возможности:
- Запись MP3 / AAC / OGG Vorbis потоков через ICY-протокол
- Два режима:
split(файл на трек) илиsingle(один файл + CUE) - ID3v2.3 теги (TIT2, TPE1) — ручная реализация без зависимостей
- Точная длительность по парсингу фреймов — не зависит от битрейта (VBR-safe)
- Разделение треков по синхромаркерам — никаких щелчков на границах
- Параллельная запись нескольких станций
- HTTP-статус: JSON-эндпоинт + SSE + HTML-страница
- Нарезка готовой записи по CUE-листу (
icecast-grab split-cue) - JSONL-логирование (stdout или файл)
- Автоматическое переподключение при обрыве
- Graceful shutdown (Ctrl+C)
- CGO_ENABLED=0 — статический бинарник, ноль внешних зависимостей
Установка
go install git.ymnuktech.ru/ymnuk/icecast-grab/cmd/grab@latest
Или сборка из исходников:
git clone <repo-url>
cd icecast-grab
CGO_ENABLED=0 go build -o icecast-grab ./cmd/grab/
Готовый бинарник можно скопировать на любой Linux-сервер (scratch-образ в Dockerfile).
Использование
Запись потока
icecast-grab -c config.yaml
icecast-grab -c config.yaml --http :8080 --log ./grab.log
CLI-флаги
| Флаг | По умолчанию | Описание |
|---|---|---|
-c / --config |
— | Путь к YAML-конфигу |
--http |
— | Адрес HTTP-сервера статуса (например :8080) |
--log |
stdout |
Файл для JSONL-лога |
Нарезка по CUE
icecast-grab split-cue -i recording.mp3 -c recording.cue -o ./splits/
Флаги split-cue
| Флаг | По умолчанию | Описание |
|---|---|---|
-i |
— | Входной аудиофайл |
-c |
— | CUE-лист |
-o |
. |
Выходная директория |
--codec |
mp3 |
Кодек: mp3, aac, ogg |
--naming |
{{.Artist}} - {{.Title}}.mp3 |
Шаблон имени файла |
Нарезка выполняется строго по границам фреймов — ни один фрейм не режется пополам.
Конфигурация
http:
listen: ":8080"
streams:
- name: "My Stream"
url: "http://example.com:8000/stream"
output:
dir: "./recordings/my-stream"
mode: "split"
naming: "{{.Date}}/{{.Time}} - {{.Artist}} - {{.Title}}.mp3"
- name: "Another Stream"
url: "http://another.example.com:8000/stream2"
output:
dir: "./recordings/another"
mode: "single"
naming: "{{.DateTime}} - {{.Name}}.mp3"
reconnect:
retry_delay: 5s
max_retries: 0
Полный пример — config.yaml.example.
Режимы записи
split — на каждый трек создаётся отдельный файл:
- Файл не создаётся, пока не получен первый
StreamTitle(нет пустых дублей) - ID3v2.3 теги:
TIT2(название),TPE1(исполнитель) - Carryover-буфер выравнивает по синхромаркеру, чтобы не было щелчков на стыках
- При смене трека старый файл закрывается, новый создаётся
single — один аудиофайл + сопутствующий .cue-лист:
- CUE-лист содержит
TITLEиINDEXдля каждого трека - Тайминги в CUE рассчитываются по фреймам (точность до сэмпла)
- Первый трек всегда начинается с
00:00:00
Шаблон имени
Доступны переменные: {{.Date}} (20260712), {{.Time}} (213608), {{.DateTime}} (20260712_213608), {{.Name}} (имя станции), {{.Artist}}, {{.Title}}.
Переподключение
При обрыве соединения (не фатальном) стрим автоматически переподключается. Для split-режима текущий файл закрывается — новый трек начнётся после восстановления. Фатальные ошибки (нет icy-metaint, неверный статус) останавливают стрим без retry.
Как это работает
ICY-протокол
- Клиент шлёт
GETс заголовкомIcy-Metadata: 1 - Сервер отвечает
200 OKи начинает слать аудио-блоки размеромicy-metaintбайт, после каждого — блок метаданных - Метаданные парсятся:
StreamTitle='Artist - Song Title' - При смене названия трека создаётся новый файл (split) или записывается CUE-запись (single)
Расчёт длительности
Битрейт в VBR-потоке плавает, поэтому считать длительность по len(chunk) / bitrate нельзя. Вместо этого:
- MP3 — парсится заголовок каждого фрейма:
samples= 1152 (MPEG1) / 576 (MPEG2),sample_rateиз таблицы. Длительность фрейма =samples / sample_rate. Битрейт не участвует. - AAC (ADTS) — 1024 сэмпла на фрейм, sample rate из 4-битного индекса в заголовке.
- OGG Vorbis —
granule_positionмежду страницами, sample rate из ID-пакета.
Для всех кодеков используется единый API: DurationOf(data, codec, &oggCtx) time.Duration.
Нарезка (split-cue)
- CUE-лист парсится:
INDEX 01 MM:SS:FF→time.Duration - Строится индекс фреймов: для каждого фрейма запоминается
{endByte, cumulativeDuration} - Для каждой CUE-записи бинарным поиском находится ближайшая граница фрейма
- Сегмент аудио пишется в новый файл с корректным ID3v2.3 заголовком
HTTP-статус
При запуске с флагом --http :8080 доступен веб-интерфейс:
| Эндпоинт | Описание |
|---|---|
GET / |
HTML-страница с таблицей статусов (SSE-обновление) |
GET /api/status |
JSON со всеми стримами |
GET /api/status/stream |
Server-Sent Events — поток обновлений |
Каждый стрим отображает: имя, статус, текущий трек, битрейт, записанные байты, текущий файл.
Сборка
Бинарник
CGO_ENABLED=0 go build -o icecast-grab ./cmd/grab/
Docker
docker build -t icecast-grab .
docker run --rm -v $(pwd)/config.yaml:/config.yaml icecast-grab -c /config.yaml
Готовый scratch-образ — около 8 MB.
Тестирование
# Все unit-тесты
go test -v -count=1 ./...
# С детектором гонок
go test -race -count=1 ./...
# Бенчмарки
go test -bench=. -benchmem -count=1 ./...
# Линтер
golangci-lint run
Интеграционные тесты с ffmpeg (проверка DurationOf vs ffprobe):
# Пропускаются автоматически, если ffmpeg не установлен
go test -v -run FFmpeg ./...
45+ тестов, race-детектор чист.
Зависимости
require gopkg.in/yaml.v3 v3.0.1
Одна зависимость — pure Go парсер YAML. Всё остальное (ICY-протокол, ID3v2, CUE, фреймовый парсинг MP3/AAC/OGG) реализовано вручную без внешних библиотек.
CI/CD
Woodpecker CI (.woodpecker.yml):
- Триггер по тегу
staticcheck+ тесты + race + бенчмарки- Сборка релизного бинарника
- Публикация в Gitea Release
- Сборка и публикация Docker-образа
Архитектура
cmd/grab/main.go — точка входа, CLI
lib/
├── config.go — YAML-конфиг
├── types.go — Codec, OutputCfg, StreamConfig
├── parser.go — ICY-метаданные, синхромаркеры, DurationOf
├── cue.go — парсинг CUE-листов
├── split.go — нарезка аудио по CUE
├── writer.go — запись файлов, ID3v2.3, CUE-генерация
├── stream.go — ICY-чтение, reconnect
├── manager.go — менеджер стримов
├── server.go — HTTP-сервер статуса
├── status.go — StreamState
├── log.go — JSONL-логгер
└── static/ — встроенная HTML-страница
Подробнее — в docs/:
Лицензия
MIT
Правовое предупреждение: Записанный контент остаётся собственностью соответствующих правообладателей. Утилита предназначена исключительно для персонального time-shifted прослушивания. Убедитесь в соблюдении применимого авторского и смежного законодательства в вашей юрисдикции. Разработчик не несёт ответственности за неправомерное использование.