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
This commit is contained in:
2026-05-21 09:45:27 +03:00
parent 735ff0828e
commit 0065057514
13 changed files with 1388 additions and 12 deletions

View File

@@ -16,6 +16,7 @@ func TestNew_InitializationAndSchema(t *testing.T) {
"_migrations",
"artist_settings",
"external_releases",
"local_albums",
"notifications_sent",
}
@@ -46,14 +47,14 @@ func TestNew_MigrationIdempotency(t *testing.T) {
// Verify tables still exist.
var count int
err = db.Conn().QueryRow(
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name IN (?,?,?,?)",
"_migrations", "artist_settings", "external_releases", "notifications_sent",
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name IN (?,?,?,?,?)",
"_migrations", "artist_settings", "external_releases", "local_albums", "notifications_sent",
).Scan(&count)
if err != nil {
t.Fatalf("query error: %v", err)
}
if count != 4 {
t.Errorf("expected 4 tables, got %d", count)
if count != 5 {
t.Errorf("expected 5 tables, got %d", count)
}
db.Close()
@@ -204,8 +205,8 @@ func TestMigrationTracking(t *testing.T) {
t.Fatalf("query migrations count: %v", err)
}
// We have 3 recorded migrations: artist_settings, external_releases, notifications_sent.
if count != 3 {
t.Errorf("expected 3 applied migrations, got %d", count)
// We have 4 recorded migrations: artist_settings, external_releases, local_albums, notifications_sent.
if count != 4 {
t.Errorf("expected 4 applied migrations, got %d", count)
}
}