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:
103
internal/database/local_albums.go
Normal file
103
internal/database/local_albums.go
Normal file
@@ -0,0 +1,103 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user