Files
NaviWatcher/docs/Specification.md
Vladimir Zagainov a8aa445d94 fix: address code review findings
- 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.
2026-07-20 06:18:21 +03:00

13 KiB
Raw Permalink Blame History

ЦЕЛЬ ПРОЕКТА

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:

  1. В веб-интерфейсе Navidrome: Settings → Users → Add User.
  2. Задать username и надёжный пароль.
  3. Убедиться, что роль имеет доступ к нужным медиатекам.
  4. Указать эти данные в config.yaml (поля user и password).

3. Архитектура системы

Проект разделен на логические модули:

  1. Navidrome Client: Взаимодействие с Subsonic API v1.16.1 (чтение артистов и локальных альбомов). Аутентификация — токенная (md5), пароль не передаётся в открытом виде. Используются эндпоинты: getArtists, getArtist, getAlbum, ping.
  2. MusicBrainz Provider: Запрос дискографий с механизмом кэширования и соблюдением Rate Limit (1 запрос в секунду).
  3. Scanner Engine: Логика нормализации строк и сравнения списков (Diff).
  4. Database Layer: Персистентное хранение данных и пользовательских фильтров.
  5. Notifier: Планировщик задач и отправка уведомлений в Telegram.
  6. Web UI: Интерфейс для просмотра находок и управления исключениями.

4. Алгоритмы и бизнес-логика

4.1. Работа с внешними данными (MusicBrainz)

Для минимизации дубликатов (разные издания одного альбома) сервис должен работать с сущностью Release Group (RG), а не с конкретными релизами.

  • Кэширование: Данные о дискографии артиста сохраняются в SQLite на 24 часа. Повторные запросы в этот период идут только в БД.

4.2. Фильтрация контента

При получении списка Release Groups из MusicBrainz применяется многоуровневый фильтр:

  1. По статусу: Игнорировать Bootleg, Promotion, Pseudo-Release.
  2. По типу (глобально): Разрешены Album, Single, EP, Compilation.
  3. По типу (персонально для артиста): Возможность отключить Single или Compilation для конкретного исполнителя через Web UI.
  4. По вторичным признакам: Опциональное игнорирование Live, Remix, Soundtrack.

4.3. Сравнение (Fuzzy Matching)

Чтобы избежать ложных срабатываний (например, «The Wall» vs «The Wall (Remastered)»), используется алгоритм:

  1. Нормализация: Приведение к нижнему регистру, удаление спецсимволов, удаление года (20xx), удаление ключевых слов в скобках (Deluxe, Anniversary, Expanded).
  2. Сравнение: Если коэффициент сходства строк выше 0.85 (настраиваемо), альбом считается «уже имеющимся».

4.4. Жизненный цикл уведомлений

  1. Найден новый релиз -> Проверка, нет ли его в таблице ignored_releases или notifications_sent.
  2. Если нет -> Добавление в очередь на отправку.
  3. Раз в сутки -> Отправка сводного сообщения в Telegram -> Пометка в notifications_sent.

5. Модель данных (SQLite)

Таблица artist_settings

Хранит параметры мониторинга для каждого артиста из Navidrome.

  • id: string (Navidrome artist ID — Primary Key)
  • name: string
  • mbid: 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: string
  • type: string (album/single/ep/compilation)
  • release_date: string
  • is_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. Программные требования и ограничения

  1. Rate Limiting: MusicBrainz API позволяет делать не более 1 запроса в секунду. Демон должен строго соблюдать этот интервал.
  2. Concurrency: Запросы к Navidrome и MusicBrainz должны выполняться в отдельных горутинах, чтобы веб-интерфейс оставался отзывчивым.
  3. Graceful Shutdown: При получении сигнала завершения (SIGTERM) демон должен корректно закрыть соединение с SQLite.
  4. Static Files: Все HTML-шаблоны должны быть встроены в бинарный файл (использование //go:embed).
  5. Docker: Проект должен быть запущен через docker compose.