Files
NaviWatcher/docs/plans/2026-05-20-navidrome-client.md
Vladimir Zagainov 0065057514 feat: add Navidrome client with Subsonic API sync
Add 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.

- Add go-subsonic dependency for Subsonic API communication
- Create internal/navidrome/client.go with NavidromeClient wrapper
  - NewClient constructor with token-based auth
  - Ping health check with HTTP status validation
  - GetArtists fetches all artists via getArtists endpoint
  - GetArtistAlbums fetches albums per artist via getArtist endpoint
- Create internal/navidrome/sync.go with sync orchestration
  - SyncArtists upserts artists into artist_settings table
  - SyncAlbums fetches and stores albums for monitored artists
- Add local_albums table (migration 003) with FK to artist_settings
- Add LocalAlbum CRUD operations in internal/database/local_albums.go
- Full test coverage: 19 tests across client and sync packages
- All tests pass, go vet and go fmt clean
2026-05-21 09:45:27 +03:00

9.3 KiB
Raw Permalink Blame History

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

  • run go get github.com/delucks/go-subsonic@latest
  • run go mod tidy
  • create internal/navidrome/client.go with NavidromeClient struct wrapping subsonic.Client
  • implement NewClient(cfg config.NavidromeConfig) (*NavidromeClient, error) constructor
  • implement Ping(ctx context.Context) error — health check via Subsonic ping endpoint
  • write tests: NewClient with valid config
  • write tests: Ping success (mock HTTP server returning valid Subsonic response)
  • write tests: Ping failure (server unreachable, invalid credentials, non-200 status)
  • run tests — must pass before task 2

Task 2: Implement artist fetching

  • in internal/navidrome/client.go, implement GetArtists(ctx context.Context) ([]ArtistInfo, error) — fetches all artists via getArtists endpoint
  • define ArtistInfo struct with ID and Name fields (mapped from Subsonic response)
  • write tests: GetArtists success with multiple artists (mock server)
  • write tests: GetArtists empty library
  • write tests: GetArtists API error handling
  • run tests — must pass before task 3

Task 3: Implement album fetching per artist

  • in internal/navidrome/client.go, implement GetArtistAlbums(ctx context.Context, artistID string) ([]AlbumInfo, error) — fetches albums via getArtist endpoint
  • define AlbumInfo struct with ID, Name, ArtistID fields
  • write tests: GetArtistAlbums success with multiple albums
  • write tests: GetArtistAlbums artist with no albums
  • write tests: GetArtistAlbums API error handling
  • run tests — before task 4

Task 4: Implement sync — artists to local DB

  • create internal/navidrome/sync.go with SyncArtists(ctx context.Context, client *NavidromeClient, db *database.DB) error
  • implement sync logic: fetch all artists from Navidrome → upsert into artist_settings table (monitored=true by default)
  • handle context cancellation gracefully
  • create internal/navidrome/sync_test.go
  • write tests: SyncArtists with empty Navidrome library
  • write tests: SyncArtists with multiple artists (verify DB state after sync)
  • write tests: SyncArtists idempotency (running twice doesn't duplicate)
  • write tests: SyncArtists context cancellation
  • run tests — must pass before task 5

Task 5: Implement sync — albums to local DB

  • in internal/navidrome/sync.go, implement SyncAlbums(ctx context.Context, client *NavidromeClient, db *database.DB) error
  • 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)
  • 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)
  • handle context cancellation gracefully
  • write tests: SyncAlbums for single artist with multiple albums
  • write tests: SyncAlbums skips unmonitored artists
  • write tests: SyncAlbums API error mid-sync (partial sync handling)
  • run tests — must pass before task 6

Task 6: Verify acceptance criteria

  • verify go-subsonic is in go.mod and go.sum
  • verify Ping works against mock server
  • verify GetArtists returns parsed artist list
  • verify GetArtistAlbums returns parsed album list
  • verify SyncArtists populates artist_settings table
  • verify SyncAlbums populates album data
  • run full test suite: go test ./... -v — all must pass
  • run go vet ./... — no issues
  • run go fmt ./... — no formatting issues
  • verify test coverage for navidrome package (70%+)

Task 7: Update documentation

  • update README.md with Navidrome setup instructions (creating service user) — already present in Quick Start; enhanced architecture table with table names
  • document any schema changes made (e.g., new tables or columns) — added local_albums table docs to Specification.md Section 5
  • 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

// 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