feat: add SyncAll periodic sync pipeline and wire Navidrome client into App

This commit is contained in:
2026-07-19 22:22:46 +03:00
parent 40c4240693
commit dc4bdcdab0
9 changed files with 417 additions and 4 deletions

View File

@@ -0,0 +1,261 @@
# Foundation Layer
## Overview
Build the foundation layer of NaviWatcher: a greenfield Go project with zero existing code. This plan covers project initialization, configuration management, SQLite database layer, and Docker deployment setup. These are the building blocks that all future modules (Navidrome client, MusicBrainz provider, Scanner, Notifier, Web UI) will depend on.
**Problem it solves:** Establishes the project skeleton, dependency management, configuration parsing, persistent storage, and containerized deployment — everything needed before any business logic can be built.
**How it integrates:** This is the first plan for the project. Future plans will build on this foundation:
- Plan 2: Navidrome client (depends on config + DB)
- Plan 3: MusicBrainz provider (depends on config + DB)
- Plan 4: Scanner engine (depends on Navidrome + MusicBrainz + DB)
- Plan 5: Notifier (depends on DB + config)
- Plan 6: Web UI (depends on DB + config)
## Context (from discovery)
- **Files/components involved:** All files are new — this is a greenfield project
- **Related patterns found:** Specification defines a 6-module architecture with YAML config, SQLite storage, and Docker deployment
- **Dependencies identified:**
- `github.com/mattn/go-sqlite3` — SQLite 3 driver
- `gopkg.in/yaml.v3` — YAML config parsing
- Go 1.21+ required
- **Current state:** Only docs exist (CLAUDE.md, README.md, License.md, docs/Specification.md). No Go code, no go.mod, no config files.
## Development Approach
- **Testing approach:** Regular (code first, then tests)
- Complete each task fully before moving to the next
- Make small, focused changes
- **CRITICAL: every task MUST include new/updated tests** for code changes in that task
- tests are not optional — they are a required part of the checklist
- write unit tests for new functions/methods
- write unit tests for modified functions/methods
- add new test cases for new code paths
- update existing test cases if behavior changes
- tests cover both success and error scenarios
- **CRITICAL: all tests must pass before starting next task** — no exceptions
- **CRITICAL: update this plan file when scope changes during implementation**
- Run tests after each change
- Maintain backward compatibility
## Testing Strategy
- **Unit tests**: required for every task (see Development Approach above)
- **E2E tests**: not applicable for foundation layer (no UI yet)
## Progress Tracking
- Mark completed items with `[x]` immediately when done
- Add newly discovered tasks with prefix
- Document issues/blockers with ⚠️ prefix
- Update plan if implementation deviates from original scope
- Keep plan in sync with actual work done
## What Goes Where
- **Implementation Steps** (`[ ]` checkboxes): tasks achievable within this codebase — code changes, tests, documentation updates
- **Post-Completion** (no checkboxes): items requiring external action — manual testing, deployment verification, third-party checks
## Implementation Steps
### Task 1: Initialize Go module and project skeleton
- [x] run `go mod init naviwatcher` in project root
- [x] create directory structure: `internal/config/`, `internal/database/`, `cmd/naviwatcher/`
- [x] create `cmd/naviwatcher/main.go` with basic entry point (parse flags, load config placeholder, graceful shutdown skeleton)
- [x] add dependencies: `go get github.com/mattn/go-sqlite3`, `go get gopkg.in/yaml.v3`
- [x] run `go mod tidy`
- [x] verify the project builds: `go build -o naviwatcher ./cmd/naviwatcher`
- [x] write tests for main.go flag parsing (if applicable)
- [x] run tests — must pass before task 2
### Task 2: Configuration management
- [x] create `internal/config/config.go` with Config struct matching spec (Server, Navidrome, MusicBrainz, Telegram, Scanner sections)
- [x] implement `LoadConfig(path string) (*Config, error)` function that reads and parses YAML
- [x] implement config validation (required fields, valid port range, valid threshold range 0.0-1.0)
- [x] create `config.yaml.example` with all fields filled with placeholder values per spec section 7
- [x] write tests for LoadConfig: valid config file
- [x] write tests for LoadConfig: missing file, malformed YAML, invalid values
- [x] write tests for config validation logic
- [x] run tests — must pass before task 3
### Task 3: Database layer — schema and migrations
- [x] create `internal/database/database.go` with DB struct and `New(dbPath string) (*DB, error)` constructor
- [x] implement schema migration system (versioned migrations table + ordered migration files or functions)
- [x] create migration 001: `artist_settings` table (id, name, ignore_singles, ignore_compilations, monitored)
- [x] create migration 002: `external_releases` table (rgid PK, artist_id, title, type, release_date, is_ignored)
- [x] create migration 003: `notifications_sent` table (rgid FK, sent_at)
- [x] implement `Close()` method with proper connection cleanup
- [x] write tests for database initialization and schema creation (use in-memory SQLite `:memory:`)
- [x] write tests for migration idempotency (running migrations twice should not fail)
- [x] write tests for Close() behavior
- [x] run tests — must pass before task 4
### Task 4: Database layer — CRUD operations for artist_settings
- [x] implement `GetArtistSettings(db *DB, id string) (*ArtistSettings, error)`
- [x] implement `SaveArtistSettings(db *DB, settings *ArtistSettings) error`
- [x] implement `GetAllArtistSettings(db *DB) ([]ArtistSettings, error)`
- [x] implement `UpdateArtistSettings(db *DB, id string, updates map[string]interface{}) error`
- [x] write tests for GetArtistSettings (found and not found cases)
- [x] write tests for SaveArtistSettings (insert and update)
- [x] write tests for GetAllArtistSettings (empty and populated)
- [x] run tests — must pass before task 5
### Task 5: Database layer — CRUD operations for external_releases
- [x] implement `GetExternalRelease(db *DB, rgid string) (*ExternalRelease, error)`
- [x] implement `SaveExternalRelease(db *DB, release *ExternalRelease) error`
- [x] implement `GetExternalReleasesByArtist(db *DB, artistID string) ([]ExternalRelease, error)`
- [x] implement `GetIgnoredReleases(db *DB) ([]ExternalRelease, error)`
- [x] implement `SetReleaseIgnored(db *DB, rgid string, ignored bool) error`
- [x] write tests for all CRUD operations (success and error cases)
- [x] write tests for cache TTL logic (if implemented at this layer)
- [x] run tests — must pass before task 6
### Task 6: Database layer — CRUD operations for notifications_sent
- [x] implement `MarkNotificationSent(db *DB, rgid string) error`
- [x] implement `IsNotificationSent(db *DB, rgid string) (bool, error)`
- [x] implement `GetUnnotifiedReleases(db *DB) ([]ExternalRelease, error)` — joins external_releases with notifications_sent to find unsent
- [x] write tests for MarkNotificationSent and IsNotificationSent
- [x] write tests for GetUnnotifiedReleases (with and without existing notifications)
- [x] run tests — must pass before task 7
### Task 7: Docker setup
- [x] create `Dockerfile` with multi-stage build: build stage (golang:1.25-alpine) + runtime stage (alpine:latest)
- [x] configure Dockerfile to copy config, build binary, expose port, set entrypoint
- [x] create `docker-compose.yml` with naviwatcher service, volume for SQLite DB and config
- [x] create `.dockerignore` file
- [x] verify Docker image builds: `docker compose build`
- [x] verify container starts and health check passes (container starts, loads config, runs successfully)
- [x] run full test suite — must pass before task 8
### Task 8: Verify acceptance criteria
- [x] verify Go module builds cleanly with `go build`
- [x] verify config loads and validates from YAML file
- [x] verify all 3 database tables are created via migrations
- [x] verify all CRUD operations work against in-memory SQLite
- [x] verify Docker image builds and container starts
- [x] run full test suite: `go test ./... -v` — all must pass
- [x] run `go vet ./...` — no issues
- [x] run `go fmt ./...` — no formatting issues
- [x] verify test coverage is reasonable for foundation layer (70%+)
### Task 9: Final documentation update
- [x] update README.md with current build/run/test instructions (replace placeholder commands)
- [x] verify config.yaml.example is complete and matches spec
- [x] document any deviations from specification in this plan — no deviations found; module path is `naviwatcher` (local), Go 1.25.1 exceeds 1.21+ requirement
*Note: ralphex automatically moves completed plans to `docs/plans/completed/`*
## Technical Details
### Config Struct (Go)
```go
type Config struct {
Server ServerConfig `yaml:"server"`
Navidrome NavidromeConfig `yaml:"navidrome"`
MusicBrainz MusicBrainzConfig `yaml:"musicbrainz"`
Telegram TelegramConfig `yaml:"telegram"`
Scanner ScannerConfig `yaml:"scanner"`
}
type ServerConfig struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
Username string `yaml:"username"`
Password string `yaml:"password"`
}
type NavidromeConfig struct {
URL string `yaml:"url"`
User string `yaml:"user"`
Password string `yaml:"password"`
}
type MusicBrainzConfig struct {
UserAgent string `yaml:"user_agent"`
CacheTTL time.Duration `yaml:"cache_ttl"`
}
type TelegramConfig struct {
Enabled bool `yaml:"enabled"`
Token string `yaml:"token"`
ChatID string `yaml:"chat_id"`
CronSchedule string `yaml:"cron_schedule"`
}
type ScannerConfig struct {
FuzzyThreshold float64 `yaml:"fuzzy_threshold"`
IgnoreBootlegs bool `yaml:"ignore_bootlegs"`
IncludeCompilations bool `yaml:"include_compilations"`
}
```
### Database Schema (SQLite)
```sql
CREATE TABLE IF NOT EXISTS artist_settings (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
ignore_singles BOOLEAN DEFAULT 0,
ignore_compilations BOOLEAN DEFAULT 0,
monitored BOOLEAN DEFAULT 1
);
CREATE TABLE IF NOT EXISTS external_releases (
rgid TEXT PRIMARY KEY,
artist_id TEXT NOT NULL,
title TEXT NOT NULL,
type TEXT,
release_date TEXT,
is_ignored BOOLEAN DEFAULT 0
);
CREATE TABLE IF NOT EXISTS notifications_sent (
rgid TEXT NOT NULL,
sent_at DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (rgid, sent_at)
);
```
### Project Structure After Completion
```
naviwatcher-gitea/
├── CLAUDE.md
├── License.md
├── README.md
├── config.yaml.example
├── docker-compose.yml
├── Dockerfile
├── .dockerignore
├── go.mod
├── go.sum
├── cmd/
│ └── naviwatcher/
│ └── main.go
├── internal/
│ ├── config/
│ │ ├── config.go
│ │ └── config_test.go
│ └── database/
│ ├── database.go
│ ├── database_test.go
│ ├── artist_settings.go
│ ├── artist_settings_test.go
│ ├── external_releases.go
│ ├── external_releases_test.go
│ ├── notifications.go
│ └── notifications_test.go
└── docs/
├── Specification.md
└── plans/
└── 2026-05-20-foundation-layer.md
```
## Post-Completion
*Items requiring manual intervention or external systems — no checkboxes, informational only*
**Manual verification:**
- Copy `config.yaml.example` to `config.yaml` and fill in real values to test config loading
- Run the built Docker container against a real Navidrome instance (when client is implemented)
- Verify SQLite database file persists across container restarts via Docker volume
**Follow-up plans needed:**
- Plan 2: Navidrome/Subsonic API client
- Plan 3: MusicBrainz provider with rate limiting
- Plan 4: Scanner engine with fuzzy matching
- Plan 5: Telegram notifier with cron scheduling
- Plan 6: Web UI with embedded templates

View File

@@ -0,0 +1,179 @@
# Navidrome/Subsonic API Client
## Overview
Build the Navidrome client module that connects to a Navidrome server via the Subsonic API, fetches artist and album data, and syncs it into the local SQLite database. This is the first business logic module — it turns NaviWatcher from an empty skeleton into a service that can actually read your music library.
**Problem it solves:** NaviWatcher needs to know what artists and albums exist in the user's Navidrome instance to later compare against MusicBrainz discographies. This module provides that data.
**How it integrates:**
- Depends on: `internal/config` (NavidromeConfig), `internal/database` (CRUD operations)
- Will be consumed by: Plan 4 (Scanner engine — compares local albums vs MusicBrainz)
- The `run()` function in `main.go` will later be expanded to initialize and trigger the sync
## Context (from discovery)
- **Files/components involved:**
- New: `internal/navidrome/client.go`, `internal/navidrome/client_test.go`, `internal/navidrome/sync.go`, `internal/navidrome/sync_test.go`
- Existing: `internal/config/config.go` (NavidromeConfig with URL, User, Password), `internal/database/` (ArtistSettings CRUD, full DB layer), `cmd/naviwatcher/main.go` (run() skeleton)
- **Related patterns found:**
- Go module name: `naviwatcher`
- DB uses `database/sql` + `go-sqlite3` with WAL mode and foreign keys
- `ArtistSettings` struct: ID, Name, IgnoreSingles, IgnoreCompilations, Monitored
- Graceful shutdown via `context.Context` pattern in main.go
- **Dependencies to add:** `github.com/delucks/go-subsonic` (Subsonic API client library — supports getArtists, getArtist, getAlbum, ping; tested on Navidrome)
- **Auth method:** Subsonic token-based auth: `md5(password + salt)`, password never sent in plaintext
- **Subsonic API version:** v1.16.1
## Development Approach
- **Testing approach:** Regular (code first, then tests)
- Complete each task fully before moving to the next
- Make small, focused changes
- **CRITICAL: every task MUST include new/updated tests** for code changes in that task
- **CRITICAL: all tests must pass before starting next task** — no exceptions
- **CRITICAL: update this plan file when scope changes during implementation**
- Run tests after each change
## Testing Strategy
- **Unit tests:** required for every task
- Use `httptest.NewServer` to mock Subsonic API responses for client tests
- Use in-memory SQLite (`:memory:`) for sync tests
- Tests cover: success cases, API errors, malformed responses, empty libraries, context cancellation
## Progress Tracking
- Mark completed items with `[x]` immediately when done
- Add newly discovered tasks with prefix
- Document issues/blockers with ⚠️ prefix
## What Goes Where
- **Implementation Steps** (`[ ]` checkboxes): code changes, tests, documentation
- **Post-Completion** (no checkboxes): manual testing against real Navidrome
## Implementation Steps
### Task 1: Add go-subsonic dependency and create client wrapper
- [x] run `go get github.com/delucks/go-subsonic@latest`
- [x] run `go mod tidy`
- [x] create `internal/navidrome/client.go` with `NavidromeClient` struct wrapping `subsonic.Client`
- [x] implement `NewClient(cfg config.NavidromeConfig) (*NavidromeClient, error)` constructor
- [x] implement `Ping(ctx context.Context) error` — health check via Subsonic ping endpoint
- [x] write tests: NewClient with valid config
- [x] write tests: Ping success (mock HTTP server returning valid Subsonic response)
- [x] write tests: Ping failure (server unreachable, invalid credentials, non-200 status)
- [x] run tests — must pass before task 2
### Task 2: Implement artist fetching
- [x] in `internal/navidrome/client.go`, implement `GetArtists(ctx context.Context) ([]ArtistInfo, error)` — fetches all artists via `getArtists` endpoint
- [x] define `ArtistInfo` struct with ID and Name fields (mapped from Subsonic response)
- [x] write tests: GetArtists success with multiple artists (mock server)
- [x] write tests: GetArtists empty library
- [x] write tests: GetArtists API error handling
- [x] run tests — must pass before task 3
### Task 3: Implement album fetching per artist
- [x] in `internal/navidrome/client.go`, implement `GetArtistAlbums(ctx context.Context, artistID string) ([]AlbumInfo, error)` — fetches albums via `getArtist` endpoint
- [x] define `AlbumInfo` struct with ID, Name, ArtistID fields
- [x] write tests: GetArtistAlbums success with multiple albums
- [x] write tests: GetArtistAlbums artist with no albums
- [x] write tests: GetArtistAlbums API error handling
- [x] run tests — before task 4
### Task 4: Implement sync — artists to local DB
- [x] create `internal/navidrome/sync.go` with `SyncArtists(ctx context.Context, client *NavidromeClient, db *database.DB) error`
- [x] implement sync logic: fetch all artists from Navidrome → upsert into `artist_settings` table (monitored=true by default)
- [x] handle context cancellation gracefully
- [x] create `internal/navidrome/sync_test.go`
- [x] write tests: SyncArtists with empty Navidrome library
- [x] write tests: SyncArtists with multiple artists (verify DB state after sync)
- [x] write tests: SyncArtists idempotency (running twice doesn't duplicate)
- [x] write tests: SyncArtists context cancellation
- [x] run tests — must pass before task 5
### Task 5: Implement sync — albums to local DB
- [x] in `internal/navidrome/sync.go`, implement `SyncAlbums(ctx context.Context, client *NavidromeClient, db *database.DB) error`
- [x] implement sync logic: for each monitored artist, fetch albums → store in `local_albums` table (new table, clean separation from `external_releases` which is for MusicBrainz data)
- [x] **Schema decision:** Added `local_albums` table (id TEXT PK, artist_id TEXT FK→artist_settings.id, title TEXT) — clean separation of concerns (Option 1 from plan)
- [x] handle context cancellation gracefully
- [x] write tests: SyncAlbums for single artist with multiple albums
- [x] write tests: SyncAlbums skips unmonitored artists
- [x] write tests: SyncAlbums API error mid-sync (partial sync handling)
- [x] run tests — must pass before task 6
### Task 6: Verify acceptance criteria
- [x] verify go-subsonic is in go.mod and go.sum
- [x] verify Ping works against mock server
- [x] verify GetArtists returns parsed artist list
- [x] verify GetArtistAlbums returns parsed album list
- [x] verify SyncArtists populates artist_settings table
- [x] verify SyncAlbums populates album data
- [x] run full test suite: `go test ./... -v` — all must pass
- [x] run `go vet ./...` — no issues
- [x] run `go fmt ./...` — no formatting issues
- [x] verify test coverage for navidrome package (70%+)
### Task 7: Update documentation
- [x] update README.md with Navidrome setup instructions (creating service user) — already present in Quick Start; enhanced architecture table with table names
- [x] document any schema changes made (e.g., new tables or columns) — added `local_albums` table docs to Specification.md Section 5
- [x] document the `source` field decision for album storage — documented Option 1 (separate table) with rationale in Specification.md
*Note: ralphex automatically moves completed plans to `docs/plans/completed/`*
## Technical Details
### Subsonic API Authentication
The go-subsonic library handles token-based auth internally. The client constructor passes URL, user, and password — the library generates `md5(password + salt)` per request.
### Expected Data Flow
```
Navidrome Server (Subsonic API)
NavidromeClient (internal/navidrome/client.go)
│ - Ping()
│ - GetArtists() → []ArtistInfo
│ - GetArtistAlbums() → []AlbumInfo
Sync (internal/navidrome/sync.go)
│ - SyncArtists() → artist_settings table
│ - SyncAlbums() → local_albums or external_releases table
SQLite Database
```
### Mock Server Pattern for Tests
```go
// Create a test HTTP server that returns Subsonic XML responses
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/xml")
w.Write([]byte(`<subsonic-response status="ok" version="1.16.1">
<artists>
<index name="A">
<artist id="1" name="Artist One"/>
<artist id="2" name="Artist Two"/>
</index>
</artists>
</subsonic-response>`))
}))
defer server.Close()
```
### Schema Decision Needed (Task 5)
The current `external_releases` table is designed for MusicBrainz data. Local albums from Navidrome need a home. Options:
1. **Add `local_albums` table** — clean separation, but adds a table
2. **Add `source` column to `external_releases`** — simpler, but mixes concerns
3. **Reuse `external_releases` with a flag** — similar to option 2
The implementation should pick one and document it. Option 1 (separate table) is recommended for clean separation of concerns.
## Post-Completion
*Items requiring manual intervention or external systems*
**Manual verification:**
- Set up a real Navidrome instance with test music library
- Configure config.yaml with real credentials
- Run the sync and verify artists/albums appear in the database
- Check that unmonitored artists are skipped during album sync
**Follow-up plans needed:**
- Plan 3: MusicBrainz provider with rate limiting
- Plan 4: Scanner engine with fuzzy matching (depends on Navidrome client + MusicBrainz)
- Plan 5: Telegram notifier
- Plan 6: Web UI

View File

@@ -0,0 +1,134 @@
# 2026-05-21-musicbrainz-provider
## Overview
Implement a MusicBrainz API provider with strict 1 request/second rate limiting, 24-hour caching of Release Group data, and filtering capabilities as per specification. The provider will fetch artist discographies from MusicBrainz, normalize the data, and store it in the external_releases table for use by the scanner engine.
## Context (from discovery)
- Files/components involved: internal/musicbrainz/ package (new), database schema updates, main.go integration
- Related patterns found: Follows the internal/navidrome/ pattern with client.go, sync.go, and model separation
- Dependencies identified: Will add golang.org/x/time/rate for rate limiting, use net/http for API calls
## Development Approach
- **Testing approach**: Regular (code first, then tests)
- Complete each task fully before moving to the next
- Make small, focused changes
- **CRITICAL: every task MUST include new/updated tests** for code changes in that task
- tests are not optional - they are a required part of the checklist
- write unit tests for new functions/methods
- write unit tests for modified functions/methods
- add new test cases for new code paths
- update existing test cases if behavior changes
- tests cover both success and error scenarios
- **CRITICAL: all tests must pass before starting next task** - no exceptions
- **CRITICAL: update this plan file when scope changes during implementation**
- Run tests after each change
- Maintain backward compatibility
## Testing Strategy
- **Unit tests**: required for every task (see Development Approach above)
- **E2E tests**: if project has UI-based e2e tests (Playwright, Cypress, etc.):
- UI changes → add/update e2e tests in same task as UI code
- Backend changes supporting UI → add/update e2e tests in same task
- Treat e2e tests with same rigor as unit tests (must pass before next task)
- Store e2e tests alongside unit tests (or in designated e2e directory)
## Progress Tracking
- Mark completed items with `[x]` immediately when done
- Add newly discovered tasks with prefix
- Document issues/blockers with ⚠️ prefix
- Update plan if implementation deviates from original scope
- Keep plan in sync with actual work done
## What Goes Where
- **Implementation Steps** (`[ ]` checkboxes): tasks achievable within this codebase - code changes, tests, documentation updates
- **Post-Completion** (no checkboxes): items requiring external action - manual testing, changes in consuming projects, deployment configs, third-party verifications
- **Checkbox placement**: Checkboxes belong only in Task sections (`### Task N:` or `### Iteration N:`). Do not put checkboxes in Success criteria, Overview, or Context — they cause extra loop iterations.
## Implementation Steps
### Task 1: Create MusicBrainz client and data models
- [x] create `internal/musicbrainz/client.go` with MusicBrainzClient struct wrapping net/http.Client
- [x] implement constructor taking config and rate limiter
- [x] create `internal/musicbrainz/model.go` with structs for MusicBrainz API responses (ReleaseGroup, Artist, etc.)
- [x] implement XML parsing functions for MusicBrainz responses
- [x] write tests for XML parsing (success + error cases)
- [x] write tests for client constructor and basic API call structure
- [x] run tests - must pass before next task
### Task 2: Implement rate limiting and caching layer
- [x] add golang.org/x/time/rate dependency to go.mod
- [x] implement rate limiter using golang.org/x/time/rate.NewLimiter(1, 1) for 1 req/sec
- [x] create wrapper method for rate-limited HTTP GET requests
- [x] implement caching check: query database for existing Release Group data within TTL
- [x] write tests for rate limiting behavior (timing tests)
- [x] write tests for cache hit/miss logic
- [x] run tests - must pass before next task
### Task 3: Implement MusicBrainz API endpoints and filtering
- [x] implement GetArtistReleaseGroups(artistMBID string) method
- [x] apply filters: exclude Bootleg/Promotion/Pseudo-Release status
- [x] apply type filters: include Album/Single/EP/Compilation only
- [x] implement per-artist type filtering hooks (placeholder for Web UI integration)
- [x] normalize artist names and titles (remove special characters, years, brackets)
- [x] write tests for filtering logic (table-driven test cases)
- [x] write tests for normalization functions
- [x] run tests - must pass before next task
### Task 4: Implement database integration and sync orchestration
- [x] create `internal/musicbrainz/sync.go` with SyncArtistDiscography function
- [x] implement upsert logic: INSERT OR REPLACE into external_releases table
- [x] add cached_at column to external_releases table via migration (done in Task 3 as migration 005)
- [x] implement context.Context support for cancellation
- [x] write tests for database upsert operations
- [x] write integration tests with in-memory SQLite
- [x] run tests - must pass before next task
### Task 5: Wire up provider in application entry point
- [x] update `cmd/naviwatcher/main.go` to initialize MusicBrainz client
- [x] add MusicBrainz client to application context/dependencies
- [x] ensure graceful shutdown includes closing HTTP client connections
- [x] update config validation to ensure MusicBrainz.UserAgent is set
- [x] write tests for main.go integration (startup/shutdown)
- [x] run tests - must pass before next task
### Task 6: Verify acceptance criteria and run full test suite
- [x] verify all requirements from Overview are implemented
- [x] verify edge cases are handled (network errors, invalid responses, rate limit blocking)
- [x] run full test suite (unit tests)
- [x] run linter - all issues must be fixed
- [x] verify test coverage meets project standard (80%+)
## Technical Details
### Data Structures
- MusicBrainzClient: wraps *http.Client with rate limiter and config
- ExternalRelease: matches existing database struct with addition of CachedAt time.Time
- MusicBrainz API Response Models: ReleaseGroup, Artist, etc. based on XML schema
### Parameters and Formats
- Rate Limiter: 1 request per second burst size of 1 (strict limit)
- Cache TTL: configurable via MusicBrainzConfig.CacheTTL (default 24h)
- API Endpoint: https://musicbrainz.org/ws/2/ with proper User-Agent header
- Response Format: XML parsing of MusicBrainz Web Service responses
### Processing Flow
1. SyncArtistDiscography called with MusicBrainz Artist ID
2. Check cache: query external_releases for RGIDs with cached_at within TTL
3. If cache miss or expired: call MusicBrainz API with rate limiting
4. Parse XML response into ReleaseGroup models
5. Apply status/type filtering (Bootleg/Promotion/Pseudo-Release excluded)
6. Apply per-artist type filtering ( Singles/Compilations toggle via Web UI)
7. Normalize strings (remove special chars, years, brackets for fuzzy matching)
8. Upsert each Release Group to external_releases with current timestamp
9. Return list of Release Groups for scanner consumption
## Post-Completion
*Items requiring manual intervention or external systems - no checkboxes, informational only*
**Manual verification** (if applicable):
- Manual testing of rate limiting under load
- Verify cache expiration behavior over time
- Test with real MusicBrainz API to ensure compliance with their usage policy
- Performance testing of XML parsing and filtering logic
**External system updates** (if applicable):
- None - this is a standalone provider implementation