minor cleanup
This commit is contained in:
@@ -37,7 +37,6 @@ type args struct {
|
|||||||
Sort *SortCmd `arg:"subcommand:sort"`
|
Sort *SortCmd `arg:"subcommand:sort"`
|
||||||
SyncPlaylists *SyncPlaylists `arg:"subcommand:sync-playlist"`
|
SyncPlaylists *SyncPlaylists `arg:"subcommand:sync-playlist"`
|
||||||
Verbose bool `arg:"-v" default:"false"`
|
Verbose bool `arg:"-v" default:"false"`
|
||||||
DryRun bool `arg:"--dry-run" default:"false"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Run() {
|
func Run() {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"maps"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -69,12 +70,12 @@ type NavidromePlaylistTrack struct {
|
|||||||
MediaFileID string `json:"mediaFileId"` // the actual song ID
|
MediaFileID string `json:"mediaFileId"` // the actual song ID
|
||||||
}
|
}
|
||||||
|
|
||||||
type loginRequest struct {
|
type navidromeLoginRequest struct {
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type loginResponse struct {
|
type navidromeLoginResponse struct {
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
IsAdmin bool `json:"isAdmin"`
|
IsAdmin bool `json:"isAdmin"`
|
||||||
@@ -98,14 +99,14 @@ func NewNavidromeClient(baseURL, username, password string) (*NavidromeClient, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := c.login(); err != nil {
|
if err := c.login(); err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("login: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *NavidromeClient) login() error {
|
func (c *NavidromeClient) login() error {
|
||||||
body, _ := json.Marshal(loginRequest{Username: c.username, Password: c.password})
|
body, _ := json.Marshal(navidromeLoginRequest{Username: c.username, Password: c.password})
|
||||||
|
|
||||||
resp, err := c.httpClient.Post(
|
resp, err := c.httpClient.Post(
|
||||||
c.baseURL+"/auth/login",
|
c.baseURL+"/auth/login",
|
||||||
@@ -113,18 +114,18 @@ func (c *NavidromeClient) login() error {
|
|||||||
bytes.NewReader(body),
|
bytes.NewReader(body),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("navidrome: login: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return fmt.Errorf("navidrome: login: HTTP %d", resp.StatusCode)
|
return fmt.Errorf("status code: %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
var lr loginResponse
|
var lr navidromeLoginResponse
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&lr); err != nil {
|
if err := json.NewDecoder(resp.Body).Decode(&lr); err != nil {
|
||||||
return fmt.Errorf("navidrome: login decode: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
@@ -134,6 +135,7 @@ func (c *NavidromeClient) login() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Lists all playlists without the tracks
|
||||||
func (c *NavidromeClient) GetPlaylists() ([]NavidromePlaylist, error) {
|
func (c *NavidromeClient) GetPlaylists() ([]NavidromePlaylist, error) {
|
||||||
var playlists []NavidromePlaylist
|
var playlists []NavidromePlaylist
|
||||||
|
|
||||||
@@ -144,32 +146,28 @@ func (c *NavidromeClient) GetPlaylists() ([]NavidromePlaylist, error) {
|
|||||||
return playlists, nil
|
return playlists, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get a single playlist without the tracks
|
||||||
func (c *NavidromeClient) GetPlaylist(id string) (*NavidromePlaylist, error) {
|
func (c *NavidromeClient) GetPlaylist(id string) (*NavidromePlaylist, error) {
|
||||||
var pl NavidromePlaylist
|
var pl NavidromePlaylist
|
||||||
if err := c.request(http.MethodGet, "/api/playlist/"+id, nil, &pl); err != nil {
|
if err := c.request(http.MethodGet, "/api/playlist/"+id, nil, &pl); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
tracks, err := c.getPlaylistTracks(id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
pl.Tracks = tracks
|
|
||||||
|
|
||||||
return &pl, nil
|
return &pl, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *NavidromeClient) getPlaylistTracks(playlistID string) ([]NavidromePlaylistTrack, error) {
|
// Get all tracks inside a playlist
|
||||||
|
func (c *NavidromeClient) GetPlaylistTracks(playlistID string) ([]NavidromePlaylistTrack, error) {
|
||||||
var tracks []NavidromePlaylistTrack
|
var tracks []NavidromePlaylistTrack
|
||||||
if err := c.getList("/api/playlist/"+playlistID+"/tracks", nil, &tracks); err != nil {
|
if err := c.getList("/api/playlist/"+playlistID+"/tracks", nil, &tracks); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return tracks, nil
|
return tracks, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Search songs based on a custom tag named "spotifyid"
|
||||||
func (c *NavidromeClient) GetSongBySpotifyID(spotifyID string) ([]NavidromeTrack, error) {
|
func (c *NavidromeClient) GetSongBySpotifyID(spotifyID string) ([]NavidromeTrack, error) {
|
||||||
// https://navi.kapelle.org/api/song?_end=100&_order=ASC&_sort=title&_start=0&spotifyid=0NhcaHA0cyJyhk3g8tUsZP&missing=false
|
|
||||||
params := url.Values{
|
params := url.Values{
|
||||||
"spotifyid": {spotifyID},
|
"spotifyid": {spotifyID},
|
||||||
}
|
}
|
||||||
@@ -182,8 +180,9 @@ func (c *NavidromeClient) GetSongBySpotifyID(spotifyID string) ([]NavidromeTrack
|
|||||||
return songs, nil
|
return songs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add tracks to a playlist
|
||||||
func (c *NavidromeClient) AddTracks(playlistID string, songIDs []string) error {
|
func (c *NavidromeClient) AddTracks(playlistID string, songIDs []string) error {
|
||||||
body := map[string]interface{}{"ids": songIDs}
|
body := map[string]any{"ids": songIDs}
|
||||||
return c.request(http.MethodPost, "/api/playlist/"+playlistID+"/tracks", body, nil)
|
return c.request(http.MethodPost, "/api/playlist/"+playlistID+"/tracks", body, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,24 +195,25 @@ func (c *NavidromeClient) getList(path string, extra url.Values, dest any) error
|
|||||||
"_start": {strconv.Itoa(start)},
|
"_start": {strconv.Itoa(start)},
|
||||||
"_end": {strconv.Itoa(start + pageSize)},
|
"_end": {strconv.Itoa(start + pageSize)},
|
||||||
}
|
}
|
||||||
for k, vs := range extra {
|
|
||||||
params[k] = vs
|
maps.Copy(params, extra)
|
||||||
}
|
|
||||||
|
|
||||||
httpResp, err := c.do(http.MethodGet, path+"?"+params.Encode(), nil)
|
httpResp, err := c.do(http.MethodGet, path+"?"+params.Encode(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer httpResp.Body.Close()
|
defer httpResp.Body.Close()
|
||||||
|
|
||||||
c.refreshToken(httpResp)
|
c.refreshToken(httpResp)
|
||||||
|
|
||||||
if httpResp.StatusCode != http.StatusOK && httpResp.StatusCode != http.StatusPartialContent {
|
if httpResp.StatusCode != http.StatusOK && httpResp.StatusCode != http.StatusPartialContent {
|
||||||
return fmt.Errorf("navidrome: GET %s: HTTP %d", path, httpResp.StatusCode)
|
return fmt.Errorf("status code: %d", httpResp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
var page []json.RawMessage
|
var page []json.RawMessage
|
||||||
if err := json.NewDecoder(httpResp.Body).Decode(&page); err != nil {
|
if err := json.NewDecoder(httpResp.Body).Decode(&page); err != nil {
|
||||||
return fmt.Errorf("navidrome: decode %s: %w", path, err)
|
return fmt.Errorf("decode: %w", err)
|
||||||
}
|
}
|
||||||
all = append(all, page...)
|
all = append(all, page...)
|
||||||
|
|
||||||
@@ -231,19 +231,22 @@ func (c *NavidromeClient) request(method, path string, body any, dest any) error
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer httpResp.Body.Close()
|
defer httpResp.Body.Close()
|
||||||
|
|
||||||
c.refreshToken(httpResp)
|
c.refreshToken(httpResp)
|
||||||
|
|
||||||
if httpResp.StatusCode >= 300 {
|
if httpResp.StatusCode >= 300 {
|
||||||
raw, _ := io.ReadAll(httpResp.Body)
|
raw, _ := io.ReadAll(httpResp.Body)
|
||||||
return fmt.Errorf("navidrome: %s %s: HTTP %d: %s",
|
return fmt.Errorf("status code: %d: %s", httpResp.StatusCode, strings.TrimSpace(string(raw)))
|
||||||
method, path, httpResp.StatusCode, strings.TrimSpace(string(raw)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if dest != nil && httpResp.ContentLength != 0 {
|
if dest != nil && httpResp.ContentLength != 0 {
|
||||||
if err := json.NewDecoder(httpResp.Body).Decode(dest); err != nil {
|
if err := json.NewDecoder(httpResp.Body).Decode(dest); err != nil {
|
||||||
return fmt.Errorf("navidrome: decode %s: %w", path, err)
|
return fmt.Errorf("decode: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,6 +260,7 @@ func (c *NavidromeClient) do(method, path string, body any) (*http.Response, err
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.StatusCode == http.StatusUnauthorized {
|
if resp.StatusCode == http.StatusUnauthorized {
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
if err := c.login(); err != nil {
|
if err := c.login(); err != nil {
|
||||||
@@ -264,6 +268,7 @@ func (c *NavidromeClient) do(method, path string, body any) (*http.Response, err
|
|||||||
}
|
}
|
||||||
return c.doOnce(method, path, bodyBytes)
|
return c.doOnce(method, path, bodyBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,16 +277,20 @@ func (c *NavidromeClient) doOnce(method, path string, bodyBytes []byte) (*http.R
|
|||||||
if bodyBytes != nil {
|
if bodyBytes != nil {
|
||||||
bodyReader = bytes.NewReader(bodyBytes)
|
bodyReader = bytes.NewReader(bodyBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequest(method, c.baseURL+path, bodyReader)
|
req, err := http.NewRequest(method, c.baseURL+path, bodyReader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("navidrome: build request: %w", err)
|
return nil, fmt.Errorf("build request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if bodyBytes != nil {
|
if bodyBytes != nil {
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
}
|
}
|
||||||
|
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
req.Header.Set("X-ND-Authorization", "Bearer "+c.token)
|
req.Header.Set("X-ND-Authorization", "Bearer "+c.token)
|
||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
|
|
||||||
return c.httpClient.Do(req)
|
return c.httpClient.Do(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,10 +298,12 @@ func marshalBody(body any) ([]byte, error) {
|
|||||||
if body == nil {
|
if body == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
b, err := json.Marshal(body)
|
b, err := json.Marshal(body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("navidrome: marshal body: %w", err)
|
return nil, fmt.Errorf("navidrome: marshal body: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return b, nil
|
return b, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -90,11 +90,12 @@ func (c *SpotifyClient) GetPlaylist(playlistID string) (*SpotifyPlaylist, error)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
body, _ := io.ReadAll(resp.Body)
|
body, _ := io.ReadAll(resp.Body)
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return nil, fmt.Errorf("API error (%d): %s", resp.StatusCode, body)
|
return nil, fmt.Errorf("status code: %d: %s", resp.StatusCode, body)
|
||||||
}
|
}
|
||||||
|
|
||||||
var playlist SpotifyPlaylist
|
var playlist SpotifyPlaylist
|
||||||
@@ -115,6 +116,7 @@ func (c *SpotifyClient) renewToken() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Header.Set("Authorization", "Basic "+creds)
|
req.Header.Set("Authorization", "Basic "+creds)
|
||||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
|
||||||
@@ -122,11 +124,12 @@ func (c *SpotifyClient) renewToken() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
body, _ := io.ReadAll(resp.Body)
|
body, _ := io.ReadAll(resp.Body)
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return fmt.Errorf("auth failed (%d): %s", resp.StatusCode, body)
|
return fmt.Errorf("auth failed: %d: %s", resp.StatusCode, body)
|
||||||
}
|
}
|
||||||
|
|
||||||
var token spotifyTokenResponse
|
var token spotifyTokenResponse
|
||||||
@@ -158,5 +161,6 @@ func (c *SpotifyClient) doOnce(req *http.Request) (*http.Response, error) {
|
|||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
req.Header.Set("Authorization", "Bearer "+c.token)
|
req.Header.Set("Authorization", "Bearer "+c.token)
|
||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
|
|
||||||
return c.httpClient.Do(req)
|
return c.httpClient.Do(req)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user