- Honor ignore_singles/ignore_compilations at scanner read time so toggles take effect immediately on the dashboard, artist page, and digest instead of waiting for the MusicBrainz cache to expire and prune rows. - Run notifier notify synchronously in the scheduler loop to avoid overlapping read-send-mark runs double-sending the digest. - Show artist name (with ID fallback) on the archive page instead of raw IDs. - Select last_synced in GetAllArtistSettings for contract consistency. - Fix stale startPeriodicSync comment and remove redundant error var. - Remove dead ignored-branch from the artist template (never rendered). - Add tests: CSRF sameOrigin, ArtistCacheFresh, secondary_types round-trip, and scanner type-toggle filtering. - Update Specification.md schema/config to reflect mbid, last_synced, secondary_types, sync.interval, and server.public_url.
13 KiB
ЦЕЛЬ ПРОЕКТА
1. Общее описание проекта
NaviWatcher — это автономный сервисный инструмент (демон), предназначенный для автоматического мониторинга музыкальных коллекций, хранящихся в Navidrome (или любом сервере с поддержкой Subsonic API).
Основная задача: сопоставлять текущую медиатеку пользователя с полной дискографией артистов во внешних базах данных (MusicBrainz), находить отсутствующие релизы (альбомы, синглы, сборники) и уведомлять об этом пользователя через Telegram и встроенный веб-интерфейс.
2. Технологический стек
- Язык программирования: Go 1.25+
- База данных: SQLite 3 (для хранения кэша, настроек и состояний).
- HTTP-сервер: Стандартная библиотека Go (
net/http) +html/template. - Внешние зависимости (библиотеки):
github.com/delucks/go-subsonic— клиент Subsonic API (getArtists, getArtist, ping).github.com/mattn/go-sqlite3— драйвер базы данных.github.com/lithammer/fuzzysearch— библиотека для нечеткого сравнения строк.gopkg.in/yaml.v3— парсинг конфигурационных файлов.
2.1. Аутентификация в Navidrome (Subsonic API)
NaviWatcher взаимодействует с Navidrome через Subsonic API v1.16.1 (эндпоинты /rest/*).
Используется токенная аутентификация — стандартный метод Subsonic API:
- Формула токена:
token = md5(password + salt) - Salt генерируется клиентом случайно при каждом запросе.
- Пароль пользователя не передаётся в открытом виде.
- Это обеспечивает совместимость с любыми Subsonic-совместимыми серверами (Navidrome, Airsonic, Ampache).
Пример запроса:
/rest/getArtists?u=watcher_service&t=<md5_hash>&s=<random_salt>&v=1.16.1&c=NaviWatcher
Создание сервисного пользователя в Navidrome:
- В веб-интерфейсе Navidrome: Settings → Users → Add User.
- Задать username и надёжный пароль.
- Убедиться, что роль имеет доступ к нужным медиатекам.
- Указать эти данные в
config.yaml(поляuserиpassword).
3. Архитектура системы
Проект разделен на логические модули:
- Navidrome Client: Взаимодействие с Subsonic API v1.16.1 (чтение артистов и локальных альбомов). Аутентификация — токенная (md5), пароль не передаётся в открытом виде. Используются эндпоинты:
getArtists,getArtist,getAlbum,ping. - MusicBrainz Provider: Запрос дискографий с механизмом кэширования и соблюдением Rate Limit (1 запрос в секунду).
- Scanner Engine: Логика нормализации строк и сравнения списков (Diff).
- Database Layer: Персистентное хранение данных и пользовательских фильтров.
- Notifier: Планировщик задач и отправка уведомлений в Telegram.
- Web UI: Интерфейс для просмотра находок и управления исключениями.
4. Алгоритмы и бизнес-логика
4.1. Работа с внешними данными (MusicBrainz)
Для минимизации дубликатов (разные издания одного альбома) сервис должен работать с сущностью Release Group (RG), а не с конкретными релизами.
- Кэширование: Данные о дискографии артиста сохраняются в SQLite на 24 часа. Повторные запросы в этот период идут только в БД.
4.2. Фильтрация контента
При получении списка Release Groups из MusicBrainz применяется многоуровневый фильтр:
- По статусу: Игнорировать
Bootleg,Promotion,Pseudo-Release. - По типу (глобально): Разрешены
Album,Single,EP,Compilation. - По типу (персонально для артиста): Возможность отключить
SingleилиCompilationдля конкретного исполнителя через Web UI. - По вторичным признакам: Опциональное игнорирование
Live,Remix,Soundtrack.
4.3. Сравнение (Fuzzy Matching)
Чтобы избежать ложных срабатываний (например, «The Wall» vs «The Wall (Remastered)»), используется алгоритм:
- Нормализация: Приведение к нижнему регистру, удаление спецсимволов, удаление года (20xx), удаление ключевых слов в скобках (Deluxe, Anniversary, Expanded).
- Сравнение: Если коэффициент сходства строк выше 0.85 (настраиваемо), альбом считается «уже имеющимся».
4.4. Жизненный цикл уведомлений
- Найден новый релиз -> Проверка, нет ли его в таблице
ignored_releasesилиnotifications_sent. - Если нет -> Добавление в очередь на отправку.
- Раз в сутки -> Отправка сводного сообщения в Telegram -> Пометка в
notifications_sent.
5. Модель данных (SQLite)
Таблица artist_settings
Хранит параметры мониторинга для каждого артиста из Navidrome.
id: string (Navidrome artist ID — Primary Key)name: stringmbid: string (MusicBrainz Artist ID; разрешается лениво при первой синхронизации и кэшируется; миграция008_add_mbid_to_artist_settings).NULLдо первого разрешения.ignore_singles: boolean (default: false)ignore_compilations: boolean (default: false)monitored: boolean (default: true)last_synced: datetime — время последней синхронизации дискографии; сигнал свежести кэша, чтобы пустые дискографии соблюдали TTL (миграция009_add_last_synced_to_artist_settings).NULL— ещё не синхронизировался.
Таблица external_releases
Кэш релизов, найденных во внешнем мире.
rgid: string (MusicBrainz Release Group ID) — Primary Key.artist_id: string (FK)title: stringtype: string (album/single/ep/compilation)release_date: stringis_ignored: boolean (флаг скрытия из списка новинок)cached_at: datetime — время последней синхронизации/кэширования из MusicBrainz; используется для проверки TTL кэша (см. миграцию005_add_cached_at_to_external_releases). ЗначениеNULLозначает отсутствие актуального кэша.secondary_types: text — вторичные типы Release Group (Single/EP/Compilation и т.д.), через запятую; используются для фильтрации по типам наряду с первичнымtype(миграция006_add_secondary_types_to_external_releases).
Таблица local_albums
Локальные альбомы, синхронизированные из Navidrome через Subsonic API.
id: string — Subsonic album ID, Primary Key.artist_id: string — FK →artist_settings.id.title: string — название альбома.
Решение по хранению локальных альбомов: Для локальных альбомов используется отдельная таблица local_albums (вариант 1 из трёх рассмотренных). Это обеспечивает чистое разделение ответственности: external_releases хранит данные MusicBrainz (Release Groups), а local_albums — данные Navidrome. Смешивание этих сущностей в одной таблице (через колонку source или флаг) усложнило бы запросы и фильтрацию, а также привело бы к неоднородности схемы (разные типы ID, разные наборы полей).
Таблица notifications_sent
rgid: string (FK)sent_at: datetime
6. Требования к интерфейсу
6.1. Web UI (Dashboard)
- Главный экран: Список артистов, у которых есть «Missing Albums».
- Страница артиста:
- Список альбомов в Navidrome (Source: Subsonic).
- Список найденных новинок (Source: MB Cache).
- Действия: «Игнорировать этот релиз», «Игнорировать все синглы артиста».
- Страница «Архив»: Просмотр ранее проигнорированных релизов с возможностью их восстановления.
6.2. Telegram
- Сообщение должно содержать список имен артистов и количество найденных новинок.
- Ссылка на Web UI для детального просмотра.
7. Конфигурация (config.yaml)
server:
host: "0.0.0.0"
port: 8080
# Basic Auth для доступа к веб-интерфейсу
username: "admin"
password: "password123"
# Внешний адрес веб-интерфейса для ссылок в Telegram-дайджестах.
# Если пусто, используется host:port (кроме 0.0.0.0 — тогда ссылка не формируется).
public_url: "https://naviwatcher.example.com"
navidrome:
url: "http://localhost:4533"
user: "watcher_service"
password: "user_password"
# Токен и salt генерируются автоматически при каждом запросе (md5(password + salt))
musicbrainz:
user_agent: "NaviWatcher/1.0 ( mail@example.com )"
cache_ttl: 24h
telegram:
enabled: true
token: "bot_token"
chat_id: "your_chat_id"
cron_schedule: "0 10 * * *" # Каждый день в 10:00
scanner:
fuzzy_threshold: 0.85
# Периодический цикл sync+scan (длительность Go, напр. "6h", "30m"); по умолчанию 6h
sync:
interval: 6h
8. Программные требования и ограничения
- Rate Limiting: MusicBrainz API позволяет делать не более 1 запроса в секунду. Демон должен строго соблюдать этот интервал.
- Concurrency: Запросы к Navidrome и MusicBrainz должны выполняться в отдельных горутинах, чтобы веб-интерфейс оставался отзывчивым.
- Graceful Shutdown: При получении сигнала завершения (SIGTERM) демон должен корректно закрыть соединение с SQLite.
- Static Files: Все HTML-шаблоны должны быть встроены в бинарный файл (использование
//go:embed). - Docker: Проект должен быть запущен через
docker compose.