Files
NaviWatcher/docs/Specification.md
Vladimir Zagainov 8a5b58a817 fix: address code review findings
- Fix artist-ID namespace mismatch in MusicBrainz provider: SyncArtistDiscography
  now stores the canonical Navidrome artist ID (artist_settings.id) as
  external_releases.artist_id instead of the MusicBrainz MBID. Previously the
  MBID was stored, which violated the FK to artist_settings and broke the
  scanner join (local_albums.artist_id is the Navidrome ID), causing every
  external release to be falsely reported as missing and the sync insert to
  fail at runtime. getArtistFilterOptions now also resolves by the Navidrome ID.
- Resolve threshold in FindMissingReleases so the exported primitive honors the
  same zero-means-default contract as ScanArtist/ScanAll.
- Remove dead maxLen==0 guard in scanner.Similarity.
- Inline trivial buildPath helper; drop unused url import in client.go.
- Replace hand-rolled itoa with strconv.Itoa in tests.
- Rewrite SyncArtistDiscography tests to seed artist_settings with the Navidrome
  ID (tests previously seeded the MBID to mask the FK mismatch).
- Fix TestFuzzySmoke to exercise the real dependency (fuzzy.LevenshteinDistance /
  scanner.Similarity) instead of an unused API.
- Fix TestAppRun_ScanLogsMissingReleases to run the scan against a live context
  and assert the missing release is found.
- Document cached_at column in Specification.md and note startup scan / required
  musicbrainz.user_agent in README.
- Stop tracking .serena/ tooling config; add it to .gitignore.
2026-07-19 18:41:18 +03:00

161 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
ignore_bootlegs: true
include_compilations: true
```
## 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`.