package navidrome import ( "encoding/xml" "fmt" "io" "net/http" "time" "naviwatcher/internal/config" "github.com/delucks/go-subsonic" ) // ArtistInfo represents a simplified artist from the Subsonic API. type ArtistInfo struct { ID string Name string } // AlbumInfo represents a simplified album from the Subsonic API. type AlbumInfo struct { ID string Name string ArtistID string } // NavidromeClient wraps the go-subsonic Client with application-specific configuration. type NavidromeClient struct { client *subsonic.Client } // NewClient creates a new NavidromeClient from the given configuration. // It authenticates with the server immediately, returning an error if auth fails. func NewClient(cfg config.NavidromeConfig) (*NavidromeClient, error) { client := &subsonic.Client{ Client: &http.Client{Timeout: 30 * time.Second}, BaseUrl: cfg.URL, User: cfg.User, ClientName: "naviwatcher", } if err := client.Authenticate(cfg.Password); err != nil { return nil, fmt.Errorf("authenticate with navidrome: %w", err) } return &NavidromeClient{client: client}, nil } // Ping checks connectivity to the Navidrome server. // Returns nil if the server is reachable and responds with a valid Subsonic OK status. func (nc *NavidromeClient) Ping() error { resp, err := nc.client.Request("GET", "ping", nil) if err != nil { return fmt.Errorf("navidrome server is unreachable: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("navidrome server returned HTTP %d", resp.StatusCode) } // Check Subsonic application-level status: the server can return HTTP 200 // with status="failed" for auth errors or other issues. body, err := io.ReadAll(io.LimitReader(resp.Body, 4096)) if err != nil { return fmt.Errorf("read ping response: %w", err) } var parsed subsonic.Response if err := xml.Unmarshal(body, &parsed); err != nil { return fmt.Errorf("parse ping response XML: %w", err) } if parsed.Status != "ok" && parsed.Error != nil { return fmt.Errorf("navidrome ping failed: code %d: %s", parsed.Error.Code, parsed.Error.Message) } return nil } // GetArtists fetches all artists from the Navidrome server. // Returns a slice of ArtistInfo with ID and Name populated. func (nc *NavidromeClient) GetArtists() ([]ArtistInfo, error) { artists, err := nc.client.GetArtists(nil) if err != nil { return nil, fmt.Errorf("get artists: %w", err) } var result []ArtistInfo for _, index := range artists.Index { for _, artist := range index.Artist { result = append(result, ArtistInfo{ ID: artist.ID, Name: artist.Name, }) } } return result, nil } // GetArtistAlbums fetches all albums for a given artist from the Navidrome server. // The artistID should be the Subsonic ID of the artist. // Returns a slice of AlbumInfo with ID, Name, and ArtistID populated. func (nc *NavidromeClient) GetArtistAlbums(artistID string) ([]AlbumInfo, error) { artist, err := nc.client.GetArtist(artistID) if err != nil { return nil, fmt.Errorf("get artist %s: %w", artistID, err) } var result []AlbumInfo for _, album := range artist.Album { result = append(result, AlbumInfo{ ID: album.ID, Name: album.Name, ArtistID: album.ArtistID, }) } return result, nil }