Веб-приложение для навигации по записям контроля радиоэфира: поиск нужного момента, выделение фрагментов и экспорт в аудиоформат.
- Python 3.9+
- ffmpeg должен быть установлен и доступен в PATH (или путь указан в
config/settings.yaml)- macOS:
brew install ffmpeg - Ubuntu/Debian:
apt install ffmpeg - Windows:
winget install -e --id Gyan.FFmpeg
- macOS:
- прочитать документацию последовательно и полностью.
sudo apt update
sudo apt install ffmpeg python3 python3-venv python3-dev
# Для монтирования SMB-источников
sudo apt install cifs-utils
# Для Kerberos / GSSAPI (только при auth_protocol: kerberos в конфиге SMB)
sudo apt install gcc libkrb5-devРекомендуется установка через Homebrew:
# Установка Homebrew (если ещё не установлен)
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Установка Python
brew install ffmpeg pythonАльтернатива — официальный установщик с python.org.
Откройте PowerShell и установите всё через winget:
winget install Gyan.FFmpeg Python.Python.3.12После установки Python перезапустите терминал, чтобы обновился PATH, затем проверьте:
python --versiongit clone https://github.com/ykmn/apricot2
cd apricot2Warning
На всех платформах используйте виртуальное окружение — это изолирует пакеты проекта от системного Python и избавляет от ошибки externally-managed-environment (Ubuntu 23.04+ / Debian 12+).
cd apricot2
# 1. Создать окружение (один раз)
python3 -m venv .venv # Linux / macOS
python -m venv .venv # Windows
# 2. Активировать окружение (каждый раз перед работой с проектом)
source .venv/bin/activate # Linux / macOS
.venv\Scripts\activate # Windows (cmd)
.venv\Scripts\Activate.ps1 # Windows (PowerShell)
# 3. Установить зависимости
pip install -r requirements.txt
# Для Kerberos / GSSAPI (только при auth_protocol: kerberos в конфиге SMB)
pip install -r requirements-kerberos.txtПосле активации в начале строки терминала появится префикс (.venv). Команду python apricot2.py также нужно выполнять с активированным окружением.
Caution
Ошибка externally-managed-environment означает, что вы запускаете pip вне виртуального окружения на современном Linux. Решение — создать и активировать .venv как показано выше. Обходной флаг --break-system-packages использовать не рекомендуется.
config/ # Рабочая конфигурация (создаётся вами)
├── settings.yaml # Глобальные настройки
├── secret.yaml # Учётные данные SMB
├── ldap.yaml # Авторизация (опционально — если файл отсутствует, авторизация отключена)
├── users.yaml # Локальные пользователи (если Local: true в ldap.yaml)
├── stations/
│ ├── retro_fm.yaml # Одна станция = один файл
│ └── europa_plus.yaml
└── playlogs/
├── retro_fm.yaml # Один плейлог для станции = один файл
└── europa_plus.yaml
config.demo/ # Демо-конфигурация
Скопируйте config.demo/ → config/ как отправную точку:
cp -r config.demo/ config/Все логины, пароли и домены хранятся в одном файле config/secret.yaml, на который ссылаются конфиги станций по числовому id. Файл добавлен в .gitignore и никогда не попадает в репозиторий. Создавайте его сами по образцу:
# config/secret.yaml
authorization:
- id: 1
username: "logger"
password: "password1"
domain: "CORP"
- id: 2
username: "svc_radio"
password: "another_pass"
domain: "CONTOSO"
- id: 3
username: "user1"
password: "..."
domain: "EVILCORP"
auth_protocol: "kerberos" # кросс-доменный доступ: использует тикет OSПоле auth_protocol (опционально):
| Значение | Когда использовать |
|---|---|
не задано / ntlm |
Сервер в том же домене или доступен по NTLM |
kerberos |
Сервер в другом домене/лесу с трастом; машина включена в домен — Kerberos использует текущий тикет Windows, явные credentials игнорируются |
Tip
Альтернатива для отдельных каналов — password_env: "MY_ENV_VAR" (хранение пароля в переменной окружения).
Important
Защита файла от посторонних. secret.yaml содержит пароли в открытом виде — ограничьте права доступа к нему. На Linux/macOS выполните один раз после создания файла:
chmod 600 config/secret.yamlЭто разрешает чтение и запись только владельцу файла. Убедитесь, что приложение запускается от того же пользователя. Файл уже добавлен в .gitignore и не попадёт в репозиторий, но права на файловой системе нужно выставить вручную.
В файле конфигурации станции для одной радиостанции может быть несколько каналов записи из разных источников: PGM, FM, SAT и т.д.; используйте YAML-массив channels:
Используйте secret: N, где N — id из config/secret.yaml:
# config/stations/retro_fm.yaml
id: retrofm
name: "Retro FM"
channels:
- id: retrofm_pgm
name: "Retro FM — PGM (mp3/64k)"
smb:
host: "fileserver.domain.local" # имя или IP сервера
share: "LOGGER" # имя сетевой шары
path: "04 Retro/PGM1" # подпапка внутри шары (необязательно)
secret: 1 # ссылка на запись в secret.yaml
folder_format: "%Y-%m-%d"
file_format: "%H-%M-%S"
file_extension: "mp3"
sample_rate: 48000
playlogs:
- retrofm
playlogs_offset: 500 # смещение плейлога относительно аудио в мс (положительное — вперёд, отрицательное — назад; по умолчанию 0)
- id: retrofm_fm
name: "Retro FM — FM (mp3/64k)"
smb:
host: "fileserver.domain.local"
share: "LOGGER"
path: "04 Retro/FM"
secret: 1
folder_format: "%Y-%m-%d"
file_format: "%H-%M-%S"
file_extension: "mp3"
sample_rate: 48000
playlogs:
- retrofm
playlogs_offset: 750Путь на сервере формируется как: \\host\share\path\<папка-даты>\<файл-время>.ext.
playlogs_offset — смещение меток плейлога относительно аудиозаписи в миллисекундах. Плейлог фиксирует момент постановки трека в эфир, тогда как в записи он слышится чуть позже (задержки кодирования, буферизации, сигнальной цепи). Положительное значение сдвигает метки вперёд (плейлог отстаёт от звука), отрицательное — назад. По умолчанию — 0.
Можно также указать учётные данные прямо в конфиге канала (не рекомендуется):
smb:
host: "fileserver"
share: "LOGGER"
path: "04 Retro/PGM1"
username: "logger"
password: "passw"
# или password_env: "MY_VAR"
domain: "CONTOSO"Если SMB-шара уже смонтирована как буква диска (Windows) или точка монтирования (Linux/Mac):
channels:
- id: retro_fm_pgm_hq
name: "Retro FM — PGM-HQ (wav/48000)"
local_path: "/mnt/retro_fm/PGM-HQ" # Linux/Mac
# local_path: "Z:\\PGM-HQ" # Windows (буква диска и двойные слеши в пути к папке)
folder_format: "%Y-%m-%d"
file_format: "%H-%M-%S"
file_extension: "wav"
sample_rate: 48000
playlogs:
- retrofmИспользуется синтаксис strftime:
| Параметр | Пример значения | Результат |
|---|---|---|
folder_format: "%Y-%m-%d" |
папки | 2026-06-03 |
file_format: "%H-%M-%S" |
файлы | 18-24-06.mp3 |
file_format: "%d%m%y_%H%M" |
файлы | 030626_1824.wav |
Если MP3-файлы записаны в JointStereo, укажите битрейт явно. Иначе длительность файлов будет рассчитана неверно и на таймлайне будут пропуски.
file_extension: "mp3"
bitrate: "64k" # 32k, 64k, 128k, 192k, 320kПри первом сканировании приложение старается определить битрейт автоматически через ffprobe и сохраняет результаты в config/detected_params.yaml. Используйте эти результаты для прафильного конфигурирования.
Каждый файл конфигурации описывает один плейлог Дигиспот Джин Вещание, обычно находящийся в папке C:\Program Files (x86)\DJin\PlayLog. Однако у каждого плейлога может быть несколько источников с разным приоритетом: основной и резервный эфирный компьютер в горячем резерве. Используйте YAML-массив sources:
Стратегия слияния источников:
- Данные приоритета 1 служат основой.
- Если в данных приоритета 1 обнаружен пробел (> 60 секунд без записей) — он заполняется записями из приоритета 2, настроенного на плейлоги с компьютера горячего резерва. Убедитесь, что в папку с плейлогом пишет только резервный эфирный Джин.
- Дубли (одно и то же событие с расхождением ≤ 5 сек по часам двух машин) автоматически исключаются.
Загрузка при старте: при запуске приложение читает и кэширует плейлоги за последние 14 дней (PRELOAD_DAYS). Сегодняшний плейлог автоматически обновляется каждые 30 минут (настраивается через playlogs.today_refresh_interval в settings.yaml).
# config/playlogs/retro_fm.yaml
id: retrofm # это название указывайте в config/stations/retro_fm.yaml
name: "Retro FM — Плейлог"
sources:
- priority: 1
local_path: "/mnt/air-1/Digispot II/DJin/PLAYLOG" # основная эфирная машина с подключенного диска
file_mask: "%Y-%m-%d.log"
encoding: "windows-1251"
delimiter: ","
header_skip_prefix: "FIELD LIST" # первый столбец строки-заголовка (пропустить)
- priority: 2
smb: # резервная машина через SMB
host: "air-2.domain.local"
share: "Digispot II" # имя сетевой шары
path: "DJin/PLAYLOG" # подпапка внутри шары (пусто = корень)
secret: 1 # логин-пароль для входа в шару из списка секретов
file_mask: "%Y-%m-%d.log"
encoding: "windows-1251"
delimiter: ","
header_skip_prefix: "FIELD LIST"
# Маппинг полей файла во внутренние поля приложения
fields:
datetime: "EventTime" # объединённое поле даты и времени
title: "ElemName" # название трека / элемента
artist: "ElemArtist" # исполнитель (если пусто — только ElemName)
cls: "ElemClass" # класс: M/J/C/P/N
db_id: "ElemDbId" # ID в базе данных → всплывает при наведении
id_number: "ElemIdNumber" # порядковый номер → всплывает при наведении
duration: "ElemLength" # длина элемента в формате MM:SS.mmm (опционально)
class_colors:
M: "#1565c0"
J: "#558b2f"
C: "#e65100"
P: "#6a1b9a"
N: "#37474f"
default: "#424242"
class_names:
M: "Музыка"
J: "Джингл"
C: "Реклама"
P: "Передача"
N: "Новости"Формат строки плейлога в UI:
Исполнитель — Название (или просто Название, если исполнитель не заполнен).
При наведении на строку показывается всплывающая подсказка: [dbID: 70501 // ID_Number: R071-01].
Кэш плейлогов хранится в cache_playlogs/{playlist_id}/{YYYY-MM-DD}.json.
Данные прошлых дат кэшируются в файлы; сегодняшний день хранится в RAM-кэше с TTL 30 минут и при перезапуске перечитывается заново.
Авторизация по умолчанию отключена. Чтобы включить её, создайте файл config/ldap.yaml. Если файл отсутствует, приложение открывается без запроса входа.
Warning
Если файл config/ldap.yaml создан, но config/users.yaml отсутствует, при каждом запуске приложение генерирует случайный одноразовый пароль для локального пользователя admin и выводит его в stderr:
[auth] WARNING: config/users.yaml not found. Temporary admin credentials — login: admin password: xK9mQz…
[auth] Create config/users.yaml to set permanent credentials.
Создайте config/users.yaml с постоянными учётными данными до публичного развёртывания.
config/ldap.yaml:
Local: true # включить локальную авторизацию
LDAP: false # доменная авторизация отключенаconfig/users.yaml:
users:
- username: admin
password: yourStrongPassword # открытый текст (не рекомендуется)
password_pbkdf2: "$pbkdf2$sha256$600000$<salt>$<hash>". # хэш пароля PBKDF2-SHA256
is_admin: true # доступ к функциям управления в меню
- username: user
password_pbkdf2: "$pbkdf2$sha256$600000$<salt>$<hash>". # хэш пароля PBKDF2-SHA256
is_admin: false # только навигация и экспортTip
Хранение паролей в виде хэша (рекомендуется).
Вместо password: используйте password_pbkdf2: — пароль хранится в виде необратимого хэша (PBKDF2-SHA256). Генерируйте хэш утилитой:
python tools/hash_password.pyСкрипт запросит пароль дважды и выведет готовый YAML-сниппет. Пример результата:
users:
- username: admin
password_pbkdf2: "$pbkdf2$sha256$600000$<salt>$<hash>"
is_admin: trueПриоритет полей: password_pbkdf2 → password (открытый текст).
Требует установки пакетов ldap3 и pycryptodome:
pip install ldap3 pycryptodomeImportant
Пакет pycryptodome нужен для работы NTLM-хэширования на Python 3.x + OpenSSL 3.x (т.к. OpenSSL 3 убрал алгоритм MD4, который использует NTLM). Пакет уже включён в requirements.txt.
config/ldap.yaml:
LDAP: true
server: "ldap://dc01.corp.local" # URL LDAP-сервера
domain: "CORP" # NetBIOS-имя домена (для NTLM-привязки)
base_dn: "DC=corp,DC=local" # Корень поиска в каталоге
# Сервисный аккаунт AD для поиска пользователей (read-only).
# Ссылается на запись в secret.yaml по полю id.
# Рекомендуется: без него диагностика ошибок менее точная.
bind_secret: 10
# Группы с правами администратора (полный доступ + меню управления).
# Любой авторизованный доменный пользователь получает доступ на просмотр автоматически.
admin_groups:
- "CN=RadioAdmins,OU=Groups,DC=corp,DC=local"Сервисный аккаунт (config/secret.yaml, запись с id: 10):
authorization:
- id: 10
username: "ldap" # read-only аккаунт в AD
password: "ldap-view-password"
domain: "CORP"Note
Права доступа: любой пользователь, успешно прошедший аутентификацию в одном из настроенных доменов, получает доступ на просмотр. Если пользователь входит в группу из admin_groups — получает права администратора.
Формат имени пользователя на странице входа:
Поддерживаются три формата:
| Формат | Поведение |
|---|---|
a.pushkin |
Поиск последовательно во всех настроенных доменах |
CORP\a.pushkin |
Поиск только в домене с name: CORP в ldap.yaml |
a.pushkin@corp.local |
Поиск только в домене с base_dn: DC=corp,DC=local (или upn_suffix: corp.local) |
Поддержка нескольких доменов:
Параметр domains: в ldap.yaml позволяет настроить несколько доменов. Попытка аутентификации идёт последовательно:
- Если пользователь не найден в домене, переходим к следующему
- Если сервер недоступен, переходим к следующему
- Если пароль неверный или нет прав доступа — сразу возвращается ошибка
Несколько доменов (кросс-доменные трасты или несколько лесов):
LDAP: true
domains:
- name: "CORP"
server: "ldap://dc01.corp.local"
base_dn: "DC=corp,DC=local"
bind_secret: 10 # id сервисного аккаунта из secret.yaml
- name: "PARTNER"
server: "ldap://dc01.partner.local"
base_dn: "DC=partner,DC=local"
bind_secret: 11
# Администраторы — применяется ко всем доменам (можно переопределить внутри каждого домена)
admin_groups:
- "CN=RadioAdmins,OU=Groups,DC=corp,DC=local"При входе без указания домена (a.pushkin) домены перебираются по порядку. При ошибке «пользователь не найден» или «сервер недоступен» попытка продолжается в следующем домене. Если пароль неверный — перебор останавливается.
Global Catalog (один лес AD, несколько доменов):
Для поиска по всем доменам леса достаточно одной записи с Global Catalog на порту 3268:
LDAP: true
server: "ldap://gc.corp.local:3268"
base_dn: "DC=corp,DC=local"
bind_secret: 10
admin_groups:
- "CN=RadioAdmins,OU=Groups,DC=corp,DC=local"Порядок проверки при входе:
- Если пользователь найден в
users.yamlпроисходит локальная проверка пароля (AD не опрашивается) - Если в локальном списке нет происходит запрос к Active Directory (один домен или список)
Можно включить оба метода одновременно. Порядок проверки фиксированный: сначала локальные пользователи, затем AD.
LDAP: true
Local: true
# ...остальные параметры LDAP...Если имя пользователя есть в users.yaml — AD не опрашивается вообще. Если нет, запрос идёт в AD.
Пользователи с is_admin: true (локальные) или входящие в admin_groups (доменные) видят в меню ☰ дополнительные пункты управления:
- Пересканировать текущий канал
- Пересканировать плейлоги станции
- Обновить конфигурацию без перезапуска
- Перезапустить сервер
После успешного входа в браузере сохраняется cookie-сессия. Продолжительность сессии задаётся параметром server.session_ttl в config/settings.yaml (в секундах, по умолчанию 604800 = 7 дней). Сессии хранятся в config/sessions.yaml и при перезапуске восстанавливаются с проверкой на истечение по времени.
server:
host: "0.0.0.0"
port: 8765
session_ttl: 604800 # продолжительность cookie-сессии в секундах (604800 = 7 дней)
log_screen: "INFO" # минимальный уровень логов в консоль (DEBUG/INFO/WARNING/ERROR)
log_file: "INFO" # минимальный уровень логов в файл logs/YYYY-mm-dd.log
ssl:
enabled: false # true — запустить сервер по HTTPS
cert: "ssl/cert.crt" # путь к сертификату (относительно корня проекта)
key: "ssl/server.key" # путь к приватному ключу
watcher:
poll_interval: 10 # интервал опроса SMB-шар в секундах
indexing:
rescan_all_on_startup: false # true — полное пересканирование всех аудиофайлов
# (без дискового кеша, с передетектом битрейта)
# при каждом запуске/перезапуске сервера
playlogs:
today_refresh_interval: 1800 # обновление сегодняшнего плейлога (секунды, по умолчанию 30 мин)
export:
default_format: "mp3"
default_bitrate: "192k"
retention_days: 3 # экспортированные файлы удаляются через N дней
cleanup_time: "03:00" # время ежедневной очистки папки export/ (HH:MM)
ffmpeg_path: "ffmpeg" # путь к ffmpeg (или просто "ffmpeg" если он есть в PATH)
ui:
timezone: "Europe/Moscow"Папка export/ очищается автоматически: при старте приложения и ежедневно в cleanup_time.
Поместите файлы сертификата и ключа в папку ssl/ и включите HTTPS в config/settings.yaml:
server:
ssl:
enabled: true
cert: "ssl/cert.crt" # путь к сертификату
key: "ssl/server.key" # путь к приватному ключуФайлы ssl/*.pem, ssl/*.key, ssl/*.crt добавлены в .gitignore и не попадают в репозиторий.
Самоподписанный сертификат (для тестирования):
openssl req -x509 -newkey rsa:4096 -keyout ssl/server.key -out ssl/cert.crt \
-days 365 -nodes -subj "/CN=localhost"Сертификат от Let's Encrypt (certbot):
certbot certonly --standalone -d your.domain.com
# Затем скопируйте или сделайте symlink:
cp /etc/letsencrypt/live/your.domain.com/fullchain.pem ssl/cert.crt
cp /etc/letsencrypt/live/your.domain.com/privkey.pem ssl/server.keyПри запуске сервер автоматически переключается на https:// и проверяет наличие файлов до старта.
Warning
Порт 443 на Linux/macOS требует прав root. Если запускаете от обычного пользователя, есть три варианта:
Вариант 1 — authbind (рекомендуется):
sudo apt install authbind
sudo touch /etc/authbind/byport/443
sudo chmod 500 /etc/authbind/byport/443
sudo chown logger /etc/authbind/byport/443
authbind --deep /home/logger/apricot2/.venv/bin/python3 apricot2.pyЗамените logger на своё имя пользователя и путь к venv при необходимости.
Вариант 2 — setcap:
sudo setcap 'cap_net_bind_service=+ep' $(readlink -f /home/logger/apricot2/venv/bin/python3)Вариант 3 — использовать порт 8443 в config/settings.yaml.
На Linux и macOS при запуске приложение автоматически монтирует все SMB-источники, настроенные в конфигурации станций и плейлогов. Каждая уникальная шара монтируется в папку mounts/<host>/<share>/ внутри директории проекта.
Повторный запуск или перезапуск безопасен: перед монтированием проверяется, подключена ли уже шара, и дубликаты не создаются.
Если монтирование завершается с ошибкой (нет прав, хост недоступен и т.п.), приложение выводит предупреждение и продолжает работу, используя smbprotocol для доступа к SMB напрямую.
Linux вызывает mount.cifs напрямую через sudo (без /bin/mount). Добавьте в /etc/sudoers через visudo:
logger ALL=(root) NOPASSWD: /usr/sbin/mount.cifs
Путь к бинарнику уточните командой which mount.cifs — на некоторых системах это /usr/sbin/mount.cifs.
[!TIP]+ Можно запустить приложение от
root(например, в Docker с--privileged): в этом случаеsudoне нужен.
macOS использует mount_smbfs, который доступен без прав администратора для текущего пользователя.
# Linux
sudo umount mounts/fileserver.domain.local/LOGGER
# macOS
umount mounts/fileserver.domain.local/LOGGER# Активируйте окружение
# Linux / macOS —
source .venv/bin/activate
# Windows
.venv\Scripts\activate
python apricot2.py
# Деактивация виртуального окружение
deactivateОткройте браузер: http://localhost:8765 (или https://localhost:8765 при включённом SSL)
Позволяет запускать приложение автоматически при старте системы и перезапускать при сбоях.
Предполагается, что проект находится в домашней папке пользователя ~/apricot2. Замените logger на своё имя пользователя.
sudo nano /etc/systemd/system/apricot2.service[Unit]
Description=Абрикос 2 — веб-интерфейс контроля радиоэфира
After=network.target
[Service]
Type=simple
User=logger
WorkingDirectory=/home/logger/apricot2
ExecStart=/home/logger/apricot2/.venv/bin/python3 apricot2.py
Restart=always
RestartSec=2
# Логи доступны через: journalctl -u apricot2
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target[!NOTE]+
ExecStartуказывает напрямую на Python внутри.venv— активировать окружение вручную не нужно.
[!IMPORTANT]+
Restart=always— обязательно при использовании кнопки «Перезапустить сервер» в меню ☰. Приложение завершается с кодом 0, аRestart=on-failureкод 0 не считает сбоем и процесс не перезапускает.
[!IMPORTANT]+ Если приложение слушает порт 443, замените в секции
[Service]:ExecStart=/usr/bin/authbind --deep /home/logger/apricot2/.venv/bin/python3 apricot2.pyПредварительно настройте authbind (см. раздел HTTPS).
# Перечитать конфигурацию systemd
sudo systemctl daemon-reload
# Включить автозапуск при старте системы
sudo systemctl enable apricot2
# Запустить прямо сейчас
sudo systemctl start apricot2sudo systemctl status apricot2 # статус и последние строки лога
sudo systemctl stop apricot2 # остановить
sudo systemctl restart apricot2 # перезапустить
sudo systemctl disable apricot2 # отключить автозапуск# В реальном времени
journalctl -u apricot2 -f
# Последние 100 строк
journalctl -u apricot2 -n 100
# За сегодня
journalctl -u apricot2 --since todayЕсли приложение запускается не от root, для авто-монтирования SMB нужно разрешить пользователю использовать mount.cifs без пароля. Добавьте в /etc/sudoers через visudo:
logger ALL=(root) NOPASSWD: /sbin/mount.cifs
Либо смонтируйте SMB-шары через /etc/fstab с опцией _netdev — тогда они будут доступны до старта сервиса и авто-монтирование в apricot2.py просто подтвердит, что шара уже подключена.
cd ~/apricot2 && git pull && sudo systemctl restart apricot2.serviceВ строке управления под заголовком:
- Поле Фильтр… — введите часть названия для быстрого поиска
- Кнопка ▾ — открыть полный список каналов (сгруппирован по станциям)
- Кнопка ⧉ — скопировать путь к папке канала в буфер обмена (
\\server\share\pathдля SMB, локальный путь для смонтированных шар и локальных папок) - Кнопка # — скопировать ссылку на текущее состояние. Ссылка содержит идентификатор канала и позицию таймлайна (
?ch=…&t=…) - Двойной клик на названии канала — скопировать название в буфер обмена
| Ряд | Масштаб | Прокрутка |
|---|---|---|
| 1 | Секунды | Посекундно |
| 2 | Минуты | Поминутно |
| 3 | Часы | Почасово |
| 4 | Дни | Подневно |
| 5 | Месяцы | Помесячно |
- Перетащите любой ряд влево/вправо для перемотки
- Колёсико мыши — прокрутка на нужном ряду
- ⟳ — прыжок к текущему времени
- Горячие клавиши:
S— фокус на поле выбора канала;[/]— метка In/Out;0— Play/Pause;+/-— шаг по строке таймлайна
- Синие ячейки — запись есть
- Тёмные ячейки — запись отсутствует
- Жёлтая подсветка — выделенный фрагмент
- Перейдите к началу нужного момента
- Нажмите In (установить начало)
- Перемотайте к концу фрагмента
- Нажмите Out (установить конец)
- Нажмите Добавить фрагмент
- Нажмите ↓ рядом с записью в списке фрагментов
- В диалоге экспорта:
- По умолчанию выбирается оригинальный формат канала — экспорт выполняется максимально быстро без перекодирования и без потерь качества
- При выборе другого формата — выполняется перекодирование с указанными параметрами
- Скачать всё — экспортирует и скачивает все фрагменты из списка последовательно
Имя экспортируемого файла: {Название канала} YYYY-mm-dd HH-MM-SS.fmt
Пример: Europa Plus PGM 2026-06-03 14-30-00.mp3
Недопустимые символы в названии канала заменяются пробелом.
Экспортированные файлы хранятся на сервере в папке export/ и автоматически удаляются через retention_days дней.
Список фрагментов сохраняется в localStorage браузера и не сбрасывается при обновлении страницы. При следующем открытии браузера все ранее добавленные фрагменты восстанавливаются автоматически.
- ▶ — воспроизведение с текущей позиции
- ⏸ — пауза
- Во время воспроизведения таймлайн скроллится через центральную линию в реальном времени
- Перемотка во время воспроизведения — прокрутите таймлайн колёсиком/перетащите или измените дату/время: анимация остановится на новой позиции, через 250 мс начинается буферизация и аудио перескакивает на выбранный момент
- Во время буферизации в строке статуса появляется мигающий индикатор Буферизация…, который гаснет в момент реального начала звука. Для SMB-каналов воспроизведение начинается почти мгновенно: сервер открывает файл как поток и сразу передаёт нужный фрагмент в ffmpeg — без скачивания файла целиком.
- Если канал записывается фрагментами с перерывами (запись только при наличии сигнала), пропуски между фрагментами внутри выбранного диапазона озвучиваются тишиной соответствующей длительности — это удерживает положение плейхеда на таймлайне синхронным с реально звучащим содержимым.
- Восстановление состояния — при повторном открытии страницы автоматически восстанавливаются выбранный канал, позиция таймлайна, маркеры выделения и список фрагментов. Состояние сохраняется на сервере в
config/sessions-ui.yamlи привязано к имени пользователя, поэтому работает в любом браузере и на любом устройстве.
Левая панель показывает плейлог для выбранного канала в окне ±3 часа от текущей позиции:
- Каждая строка окрашена цветом класса элемента (настраивается в конфиге плейлога)
- Текущий элемент (соответствующий позиции таймлайна) выделен жирным
- При наведении на строку всплывает подсказка с ID из базы данных:
[dbID: ... // ID_Number: ...] - Кнопка ↑ — выделить элемент плейлога на таймлайне (установить метки In/Out). Длина фрагмента берётся из поля
ElemLengthплейлога; если оно не заполнено — вычисляется как расстояние до следующей записи, иначе используется заглушка 3 минуты. Shift+клик расширяет текущее выделение, включая выбранный элемент. Смещение меток относительно реального положения в аудио регулируется параметромplaylogs_offsetв конфиге канала.
Под транспортными кнопками находится строка аудио-индекса с цветными точками, по одной на канал:
- 🟢 Зелёная — канал просканирован, файлы найдены. В подсказке количество файлов.
- 🟡 Жёлтая — канал доступен, но файлы не найдены (возможно, неверный
file_formatили папка пуста). Нажмите для диагностики. - 🔴 Красная — канал недоступен (ошибка подключения или авторизации). Нажмите на точку — причина ошибки.
- ⟳ Синяя мигающая — канал сканируется прямо сейчас.
Клик по тексту «⚠ N недоступн.» показывает список всех недоступных каналов с ошибками.
Точки автоматически пересобираются при «Обновить конфигурацию без перезапуска» — удалённый из конфига канал сразу исчезает из строки, добавленный сразу появляется, без обновления страницы. Ряд точек переносится на новую строку по фактически доступной ширине контейнера.
Ниже строки аудио-индекса — строка доступности источников плейлогов, тоже по одной точке на источник:
- 🟢 Зелёная — папка источника доступна,
.log-файлы найдены и успешно парсятся - 🔴 Красная — папка недоступна, файлов нет или ни один не удалось разобрать. Нажмите — причина ошибки.
- ⟳ Синяя мигающая — источник проверяется прямо сейчас.
Источники проверяются по одному, как и аудио-каналы — точки заполняются постепенно, а не все разом, и сохраняют исходный порядок (уже проверенные не перескакивают в конец строки). Проверка запускается автоматически после загрузки плейлогов при старте и по нажатию «Пересканировать плейлоги станции». Ряд точек переносится по ширине контейнера, как и в строке статуса индекса выше.
При запуске приложение:
- Загружает индекс из
cache/*.json(мгновенно) — таймлайн сразу показывает данные - В фоне сканирует SMB-шары и обновляет индекс —
scandir()читает размер файлов из ответа SMB-сервера без отдельныхstat-запросов на каждый файл - Сохраняет обновлённый индекс обратно в кэш
Сервер следит за появлением/удалением файлов (polling каждые N секунд, настраивается через watcher.poll_interval в settings.yaml). При изменении браузер получает WS-сообщение и мгновенно обновляет цветовую индикацию таймлайна без перезагрузки страницы.
| Пункт | Действие | Доступ |
|---|---|---|
| ☀️ Светлый / 🌙 Тёмный режим | Переключение темы интерфейса | Все |
| 🌐 Язык | Переключение языка интерфейса (русский, английский, французский) | Все |
| 🔍 Пересканировать текущий канал | Принудительно перечитать список аудиофайлов | Администраторы |
| 📋 Пересканировать плейлоги станции | Очистить кэш плейлогов за последние 2 дня и перепроверить все источники плейлогов текущей станции | Администраторы |
| 🔄 Обновить конфигурацию без перезапуска | Перечитать YAML-файлы без перезапуска процесса на сервере | Администраторы |
| 🔁 Перезапустить сервер | Полный перезапуск процесса; страница обновится автоматически | Администраторы |
| 👤/👷 Имя пользователя + Выйти | Внизу меню имя текущего пользователя и кнопка «Выйти» в одной строке; значок — 👷 у администратора, 👤 у остальных | Все (только при включённой авторизации) |
apricot2/
├── app/
│ ├── main.py # FastAPI-приложение, REST API, WebSocket, auth-middleware
│ ├── auth.py # Авторизация: сессии, локальные пользователи, LDAP/AD
│ ├── config.py # Загрузка YAML-конфигов, разрешение secret.yaml
│ ├── smb_client.py # Единый слой доступа: локальный путь или SMB
│ ├── smb_mount.py # Авто-монтирование SMB при запуске (Linux / macOS)
│ ├── file_index.py # Индекс аудиофайлов в памяти, polling, кэш
│ ├── audio.py # Стриминг и экспорт через ffmpeg (copy / transcode)
│ ├── audio_probe.py # Автоопределение параметров через ffprobe
│ ├── cache.py # Дисковый кэш аудио-индекса (cache_audio/*.json)
│ ├── playlist.py # Чтение CSV-плейлогов
│ └── models.py # Dataclass-модели
├── static/
│ ├── index.html # Главная страница приложения
│ ├── login.html # Страница входа (отдаётся при включённой авторизации)
│ ├── style.css
│ ├── timeline.js # Многоуровневый таймлайн (canvas)
│ └── app.js # Главная логика UI
├── config/ # Рабочая конфигурация (не в репозитории)
│ ├── settings.yaml # Общие настройки сервера
│ ├── secret.yaml # Логины и пароли для SMB
│ ├── ldap.yaml # Авторизация (опционально; при отсутствии — авторизация отключена)
│ ├── users.yaml # Локальные пользователи (опционально)
│ ├── detected_params.yaml # Найденные параметры аудиофайлов (автогенерируется)
│ ├── stations/ # Конфигурации станций и каналов записи
│ │ ├── station1.yaml
│ │ └── station2.yaml
│ └── playlogs/ # Конфигурации плейлогов
│ ├── station1.yaml
│ └── station2.yaml
├── config.demo/ # Демо-конфигурация (пример для копирования в config/)
├── export/ # Экспортированные файлы
├── mounts/ # SMB-шары, смонтированные при запуске (Linux / macOS)
│ └── <host>/<share>/ # Каждая шара — отдельная папка
├── cache_audio/ # Кэш индекса аудиофайлов (автогенерируется)
├── cache_playlogs/ # Кэш плейлогов по датам (автогенерируется)
├── requirements.txt # Зависимости Python
└── apricot2.py # Точка запуска
