You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Koito/internal/images/subsonic.go

138 lines
4.4 KiB

package images
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"github.com/gabehf/koito/internal/cfg"
"github.com/gabehf/koito/internal/logger"
"github.com/gabehf/koito/queue"
)
type SubsonicClient struct {
url string
userAgent string
authParams string
requestQueue *queue.RequestQueue
}
type SubsonicAlbumResponse struct {
SubsonicResponse struct {
Status string `json:"status"`
SearchResult3 struct {
Album []struct {
CoverArt string `json:"coverArt"`
} `json:"album"`
} `json:"searchResult3"`
} `json:"subsonic-response"`
}
type SubsonicArtistResponse struct {
SubsonicResponse struct {
Status string `json:"status"`
SearchResult3 struct {
Artist []struct {
ArtistImageUrl string `json:"artistImageUrl"`
} `json:"artist"`
} `json:"searchResult3"`
} `json:"subsonic-response"`
}
const (
subsonicAlbumSearchFmtStr = "/rest/search3?%s&f=json&query=%s&v=1.13.0&c=koito&artistCount=0&songCount=0&albumCount=1"
subsonicArtistSearchFmtStr = "/rest/search3?%s&f=json&query=%s&v=1.13.0&c=koito&artistCount=1&songCount=0&albumCount=0"
subsonicCoverArtFmtStr = "/rest/getCoverArt?%s&id=%s&v=1.13.0&c=koito"
)
func NewSubsonicClient() *SubsonicClient {
ret := new(SubsonicClient)
ret.url = cfg.SubsonicUrl()
ret.userAgent = cfg.UserAgent()
ret.authParams = cfg.SubsonicParams()
ret.requestQueue = queue.NewRequestQueue(5, 5)
return ret
}
func (c *SubsonicClient) queue(ctx context.Context, req *http.Request) ([]byte, error) {
l := logger.FromContext(ctx)
req.Header.Set("User-Agent", c.userAgent)
req.Header.Set("Accept", "application/json")
resultChan := c.requestQueue.Enqueue(func(client *http.Client, done chan<- queue.RequestResult) {
resp, err := client.Do(req)
if err != nil {
l.Debug().Err(err).Str("url", req.RequestURI).Msg("Failed to contact ImageSrc")
done <- queue.RequestResult{Err: err}
return
} else if resp.StatusCode >= 300 || resp.StatusCode < 200 {
err = fmt.Errorf("recieved non-ok status from Subsonic: %s", resp.Status)
done <- queue.RequestResult{Body: nil, Err: err}
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
done <- queue.RequestResult{Body: body, Err: err}
})
result := <-resultChan
return result.Body, result.Err
}
func (c *SubsonicClient) getEntity(ctx context.Context, endpoint string, result any) error {
l := logger.FromContext(ctx)
url := c.url + endpoint
l.Debug().Msgf("Sending request to ImageSrc: GET %s", url)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return fmt.Errorf("getEntity: %w", err)
}
l.Debug().Msg("Adding ImageSrc request to queue")
body, err := c.queue(ctx, req)
if err != nil {
l.Err(err).Msg("Subsonic request failed")
return fmt.Errorf("getEntity: %w", err)
}
err = json.Unmarshal(body, result)
if err != nil {
l.Err(err).Msg("Failed to unmarshal Subsonic response")
return fmt.Errorf("getEntity: %w", err)
}
return nil
}
func (c *SubsonicClient) GetAlbumImage(ctx context.Context, artist, album string) (string, error) {
l := logger.FromContext(ctx)
resp := new(SubsonicAlbumResponse)
l.Debug().Msgf("Finding album image for %s from artist %s", album, artist)
err := c.getEntity(ctx, fmt.Sprintf(subsonicAlbumSearchFmtStr, c.authParams, url.QueryEscape(artist+" "+album)), resp)
if err != nil {
return "", fmt.Errorf("GetAlbumImage: %v", err)
}
l.Debug().Any("subsonic_response", resp).Send()
if len(resp.SubsonicResponse.SearchResult3.Album) < 1 || resp.SubsonicResponse.SearchResult3.Album[0].CoverArt == "" {
return "", fmt.Errorf("GetAlbumImage: failed to get album art")
}
return cfg.SubsonicUrl() + fmt.Sprintf(subsonicCoverArtFmtStr, c.authParams, url.QueryEscape(resp.SubsonicResponse.SearchResult3.Album[0].CoverArt)), nil
}
func (c *SubsonicClient) GetArtistImage(ctx context.Context, artist string) (string, error) {
l := logger.FromContext(ctx)
resp := new(SubsonicArtistResponse)
l.Debug().Msgf("Finding artist image for %s", artist)
err := c.getEntity(ctx, fmt.Sprintf(subsonicArtistSearchFmtStr, c.authParams, url.QueryEscape(artist)), resp)
if err != nil {
return "", fmt.Errorf("GetArtistImage: %v", err)
}
l.Debug().Any("subsonic_response", resp).Send()
if len(resp.SubsonicResponse.SearchResult3.Artist) < 1 || resp.SubsonicResponse.SearchResult3.Artist[0].ArtistImageUrl == "" {
return "", fmt.Errorf("GetArtistImage: failed to get artist art")
}
return resp.SubsonicResponse.SearchResult3.Artist[0].ArtistImageUrl, nil
}