# ЦЕЛЬ ПРОЕКТА ## 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=&s=&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) ```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`.