Files
NaviWatcher/docs/Specification.md
Vladimir Zagainov b21bf07208 fix: address code review findings
- Store cached_at in canonical UTC layout so the cache TTL cutoff comparison is a valid time ordering (previously go-sqlite3 serialized time.Time as RFC3339, making the space-separated cutoff match only by ASCII accident; same-day expired entries were falsely served as fresh).

- Remove stale no-op scanner config keys (ignore_bootlegs, include_compilations) from config.yaml.example and docs; these fields were removed from ScannerConfig but left in configs, silently doing nothing.
2026-07-19 20:01:26 +03:00

159 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ЦЕЛЬ ПРОЕКТА
## 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 (MBID или имя)
* `name`: string
* `ignore_singles`: boolean (default: false)
* `ignore_compilations`: boolean (default: false)
* `monitored`: boolean (default: true)
### Таблица `external_releases`
Кэш релизов, найденных во внешнем мире.
* `rgid`: string (MusicBrainz Release Group ID) — Primary Key.
* `artist_id`: string (FK)
* `title`: string
* `type`: string (album/single/ep)
* `release_date`: string
* `is_ignored`: boolean (флаг скрытия из списка новинок)
* `cached_at`: datetime — время последней синхронизации/кэширования из MusicBrainz; используется для проверки TTL кэша (см. миграцию `005_add_cached_at_to_external_releases`). Значение `NULL` означает отсутствие актуального кэша.
### Таблица `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)
```yaml
server:
host: "0.0.0.0"
port: 8080
# Basic Auth для доступа к веб-интерфейсу
username: "admin"
password: "password123"
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
```
## 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`.