No description
  • Go 96.7%
  • HTML 2.8%
  • Dockerfile 0.5%
Find a file
ymnuk 17c9317ff5
Some checks failed
ci/woodpecker/tag/woodpecker Pipeline failed
Обновить Dockerfile.ci
2026-07-21 13:01:48 +03:00
cmd/grab feat: Нарезка cue-файлов 2026-07-12 22:12:42 +03:00
docs first commit 2026-07-12 19:14:08 +03:00
lib feat: Нарезка cue-файлов 2026-07-12 22:12:42 +03:00
.gitignore feat: Нарезка cue-файлов 2026-07-12 22:12:42 +03:00
.golangci.yml feat: Реализация 2026-07-12 21:15:13 +03:00
.woodpecker.yml Обновить .woodpecker.yml 2026-07-21 12:51:58 +03:00
config.yaml.example feat: Нарезка cue-файлов 2026-07-12 22:12:42 +03:00
Dockerfile feat: Реализация 2026-07-12 21:15:13 +03:00
Dockerfile.ci Обновить Dockerfile.ci 2026-07-21 13:01:48 +03:00
go.mod feat: Реализация 2026-07-12 21:15:13 +03:00
go.sum feat: Реализация 2026-07-12 21:15:13 +03:00
LICENSE first commit 2026-07-12 19:14:08 +03:00
README.md feat: Нарезка cue-файлов 2026-07-12 22:12:42 +03:00

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-протокол

  1. Клиент шлёт GET с заголовком Icy-Metadata: 1
  2. Сервер отвечает 200 OK и начинает слать аудио-блоки размером icy-metaint байт, после каждого — блок метаданных
  3. Метаданные парсятся: StreamTitle='Artist - Song Title'
  4. При смене названия трека создаётся новый файл (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 Vorbisgranule_position между страницами, sample rate из ID-пакета.

Для всех кодеков используется единый API: DurationOf(data, codec, &oggCtx) time.Duration.

Нарезка (split-cue)

  1. CUE-лист парсится: INDEX 01 MM:SS:FFtime.Duration
  2. Строится индекс фреймов: для каждого фрейма запоминается {endByte, cumulativeDuration}
  3. Для каждой CUE-записи бинарным поиском находится ближайшая граница фрейма
  4. Сегмент аудио пишется в новый файл с корректным 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 прослушивания. Убедитесь в соблюдении применимого авторского и смежного законодательства в вашей юрисдикции. Разработчик не несёт ответственности за неправомерное использование.