Files
NaviWatcher/internal/database/local_albums.go
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

104 lines
2.8 KiB
Go

package database
import (
"database/sql"
"fmt"
)
// SaveLocalAlbum inserts or replaces a local_albums row.
func SaveLocalAlbum(db *DB, album *LocalAlbum) error {
_, err := db.Conn().Exec(
"INSERT OR REPLACE INTO local_albums (id, artist_id, title) VALUES (?, ?, ?)",
album.ID, album.ArtistID, album.Title,
)
if err != nil {
return fmt.Errorf("save local album: %w", err)
}
return nil
}
// SaveLocalAlbumTx inserts or replaces a local_albums row within an existing transaction.
func SaveLocalAlbumTx(tx *sql.Tx, album *LocalAlbum) error {
_, err := tx.Exec(
"INSERT OR REPLACE INTO local_albums (id, artist_id, title) VALUES (?, ?, ?)",
album.ID, album.ArtistID, album.Title,
)
if err != nil {
return fmt.Errorf("save local album: %w", err)
}
return nil
}
// DeleteLocalAlbumsByArtistTx removes all local_albums rows for a given artist within an existing transaction.
func DeleteLocalAlbumsByArtistTx(tx *sql.Tx, artistID string) error {
_, err := tx.Exec(
"DELETE FROM local_albums WHERE artist_id = ?",
artistID,
)
if err != nil {
return fmt.Errorf("delete local albums for artist %s: %w", artistID, err)
}
return nil
}
// DeleteLocalAlbumsByArtist removes all local_albums rows for a given artist.
func DeleteLocalAlbumsByArtist(db *DB, artistID string) error {
_, err := db.Conn().Exec(
"DELETE FROM local_albums WHERE artist_id = ?",
artistID,
)
if err != nil {
return fmt.Errorf("delete local albums for artist %s: %w", artistID, err)
}
return nil
}
// GetLocalAlbumsByArtist returns all local_albums for a given artist.
func GetLocalAlbumsByArtist(db *DB, artistID string) ([]LocalAlbum, error) {
rows, err := db.Conn().Query(
"SELECT id, artist_id, title FROM local_albums WHERE artist_id = ?",
artistID,
)
if err != nil {
return nil, fmt.Errorf("query local albums for artist %s: %w", artistID, err)
}
defer rows.Close()
var results []LocalAlbum
for rows.Next() {
var a LocalAlbum
if err := rows.Scan(&a.ID, &a.ArtistID, &a.Title); err != nil {
return nil, fmt.Errorf("scan local album: %w", err)
}
results = append(results, a)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterate local albums: %w", err)
}
return results, nil
}
// GetAllLocalAlbums returns all rows from local_albums.
func GetAllLocalAlbums(db *DB) ([]LocalAlbum, error) {
rows, err := db.Conn().Query(
"SELECT id, artist_id, title FROM local_albums",
)
if err != nil {
return nil, fmt.Errorf("query all local albums: %w", err)
}
defer rows.Close()
var results []LocalAlbum
for rows.Next() {
var a LocalAlbum
if err := rows.Scan(&a.ID, &a.ArtistID, &a.Title); err != nil {
return nil, fmt.Errorf("scan local album: %w", err)
}
results = append(results, a)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterate local albums: %w", err)
}
return results, nil
}