Skip to content

Commit 19809c7

Browse files
authored
Lidarr manual import + go/lint updates (#188)
* lidarr manual import * bump go, fix linter * fix types for duration in audio/book tracks * improve marshaller, add tests
1 parent 612c6e1 commit 19809c7

File tree

13 files changed

+656
-43
lines changed

13 files changed

+656
-43
lines changed

.github/workflows/codetests.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ jobs:
4949
- name: golangci-lint
5050
uses: golangci/golangci-lint-action@v9
5151
with:
52-
version: v2.3
52+
version: v2.9
5353
# Runs golangci-lint on linux against linux and windows.
5454
golangci-linux:
5555
strategy:
@@ -67,4 +67,4 @@ jobs:
6767
- name: golangci-lint
6868
uses: golangci/golangci-lint-action@v9
6969
with:
70-
version: v2.3
70+
version: v2.9

.golangci.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ linters:
99
- tagliatelle
1010
- noinlineerr
1111
- wsl
12+
# fix these
13+
- modernize
14+
- perfsprint
15+
- godoclint
1216
settings:
1317
wsl_v5:
1418
allow-first-in-block: true

go.mod

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
module golift.io/starr
22

3-
go 1.24.0
3+
go 1.25.7
4+
5+
toolchain go1.26.0
46

57
require golang.org/x/net v0.49.0 // publicsuffix, cookiejar.
68

helpers.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ func ClientWithDebug(timeout time.Duration, verifySSL bool, logConfig debuglog.C
5959
}
6060

6161
// Itoa converts an int64 to a string.
62+
//
6263
// Deprecated: Use starr.Str() instead.
6364
func Itoa(v int64) string {
6465
return Str(v)
@@ -101,12 +102,14 @@ func False() *bool {
101102
}
102103

103104
// String returns a pointer to a string.
105+
//
104106
// Deprecated: Use Ptr() function instead.
105107
func String(s string) *string {
106108
return &s
107109
}
108110

109111
// Int64 returns a pointer to the provided integer.
112+
//
110113
// Deprecated: Use Ptr() function instead.
111114
func Int64(s int64) *int64 {
112115
return &s

lidarr/command.go

Lines changed: 74 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,24 +23,46 @@ type CommandRequest struct {
2323
ArtistID int64 `json:"artistId,omitempty"`
2424
}
2525

26+
// ManualImportFile is one file in a ManualImport command request.
27+
type ManualImportFile struct {
28+
Path string `json:"path"`
29+
ArtistID int64 `json:"artistId"`
30+
AlbumID int64 `json:"albumId"`
31+
AlbumReleaseID int64 `json:"albumReleaseId"`
32+
TrackIDs []int64 `json:"trackIds"`
33+
Quality *starr.Quality `json:"quality"`
34+
IndexerFlags int `json:"indexerFlags"`
35+
DownloadID string `json:"downloadId"`
36+
DisableReleaseSwitching bool `json:"disableReleaseSwitching"`
37+
}
38+
39+
// ManualImportCommandRequest is the body for the ManualImport command (POST /api/v1/command).
40+
// It triggers Lidarr to import the listed files.
41+
type ManualImportCommandRequest struct {
42+
Name string `json:"name"` // "ManualImport"
43+
Files []*ManualImportFile `json:"files"`
44+
ImportMode string `json:"importMode"` // "auto"
45+
ReplaceExistingFiles bool `json:"replaceExistingFiles"`
46+
}
47+
2648
// CommandResponse comes from the /api/v1/command endpoint.
2749
type CommandResponse struct {
28-
ID int64 `json:"id"`
29-
Name string `json:"name"`
30-
CommandName string `json:"commandName"`
31-
Message string `json:"message,omitempty"`
32-
Priority string `json:"priority"`
33-
Status string `json:"status"`
34-
Queued time.Time `json:"queued"`
35-
Started time.Time `json:"started,omitempty"`
36-
Ended time.Time `json:"ended,omitempty"`
37-
StateChangeTime time.Time `json:"stateChangeTime,omitempty"`
38-
LastExecutionTime time.Time `json:"lastExecutionTime,omitempty"`
39-
Duration string `json:"duration,omitempty"`
40-
Trigger string `json:"trigger"`
41-
SendUpdatesToClient bool `json:"sendUpdatesToClient"`
42-
UpdateScheduledTask bool `json:"updateScheduledTask"`
43-
Body map[string]interface{} `json:"body"`
50+
ID int64 `json:"id"`
51+
Name string `json:"name"`
52+
CommandName string `json:"commandName"`
53+
Message string `json:"message,omitempty"`
54+
Priority string `json:"priority"`
55+
Status string `json:"status"`
56+
Queued time.Time `json:"queued"`
57+
Started time.Time `json:"started,omitempty"`
58+
Ended time.Time `json:"ended,omitempty"`
59+
StateChangeTime time.Time `json:"stateChangeTime,omitempty"`
60+
LastExecutionTime time.Time `json:"lastExecutionTime,omitempty"`
61+
Duration string `json:"duration,omitempty"`
62+
Trigger string `json:"trigger"`
63+
SendUpdatesToClient bool `json:"sendUpdatesToClient"`
64+
UpdateScheduledTask bool `json:"updateScheduledTask"`
65+
Body map[string]any `json:"body"`
4466
}
4567

4668
// GetCommands returns all available Lidarr commands.
@@ -106,3 +128,39 @@ func (l *Lidarr) GetCommandStatusContext(ctx context.Context, commandID int64) (
106128

107129
return &output, nil
108130
}
131+
132+
// SendManualImportCommand sends the ManualImport command to import the given files (e.g. after FLAC+CUE split).
133+
func (l *Lidarr) SendManualImportCommand(cmd *ManualImportCommandRequest) (*CommandResponse, error) {
134+
return l.SendManualImportCommandContext(context.Background(), cmd)
135+
}
136+
137+
// SendManualImportCommandContext sends the ManualImport command to import the given files.
138+
func (l *Lidarr) SendManualImportCommandContext(
139+
ctx context.Context, cmd *ManualImportCommandRequest,
140+
) (*CommandResponse, error) {
141+
var output CommandResponse
142+
143+
if cmd == nil || len(cmd.Files) == 0 {
144+
return &output, nil
145+
}
146+
147+
if cmd.Name == "" {
148+
cmd.Name = "ManualImport"
149+
}
150+
151+
if cmd.ImportMode == "" {
152+
cmd.ImportMode = "auto"
153+
}
154+
155+
var buf bytes.Buffer
156+
if err := json.NewEncoder(&buf).Encode(cmd); err != nil {
157+
return nil, fmt.Errorf("json.Marshal(%s): %w", bpCommand, err)
158+
}
159+
160+
req := starr.Request{URI: bpCommand, Body: &buf}
161+
if err := l.PostInto(ctx, req, &output); err != nil {
162+
return nil, fmt.Errorf("api.Post(%s): %w", &req, err)
163+
}
164+
165+
return &output, nil
166+
}

lidarr/command_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package lidarr_test
22

33
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
47
"net/http"
58
"path"
69
"testing"
@@ -146,3 +149,91 @@ func TestSendCommand(t *testing.T) {
146149
})
147150
}
148151
}
152+
153+
func TestSendManualImportCommandContext(t *testing.T) {
154+
t.Parallel()
155+
156+
somedate := time.Now().Add(-36 * time.Hour).Round(time.Millisecond).UTC()
157+
datejson, _ := somedate.MarshalJSON()
158+
159+
manualImportReq := &lidarr.ManualImportCommandRequest{
160+
Name: "ManualImport",
161+
ImportMode: "auto",
162+
ReplaceExistingFiles: true,
163+
Files: []*lidarr.ManualImportFile{{
164+
Path: "/music/artist/album/01-track.flac",
165+
ArtistID: 329,
166+
AlbumID: 2826,
167+
AlbumReleaseID: 11727,
168+
TrackIDs: []int64{220502},
169+
Quality: &starr.Quality{Quality: &starr.BaseQuality{ID: 21, Name: "FLAC 24bit"}, Revision: &starr.QualityRevision{Version: 1, Real: 0, IsRepack: false}},
170+
IndexerFlags: 0,
171+
DownloadID: "b709f04aab654403bff7357c532c681a",
172+
DisableReleaseSwitching: false,
173+
}},
174+
}
175+
expectedBodyBuf := new(bytes.Buffer)
176+
require.NoError(t, json.NewEncoder(expectedBodyBuf).Encode(manualImportReq))
177+
expectedBody := expectedBodyBuf.String()
178+
179+
tests := []*starrtest.MockData{
180+
{
181+
Name: "200",
182+
ExpectedPath: path.Join("/", starr.API, lidarr.APIver, "command"),
183+
ResponseStatus: http.StatusOK,
184+
ResponseBody: `{"id":99,"name":"ManualImport","commandName":"ManualImport","message":` +
185+
`"","priority":"normal","status":"queued","queued":` + string(datejson) +
186+
`,"started":` + string(datejson) + `,"ended":` + string(datejson) +
187+
`,"stateChangeTime":` + string(datejson) + `,"lastExecutionTime":` + string(datejson) +
188+
`,"duration":"","trigger":"manual","sendUpdatesToClient":true,"updateScheduledTask":false,"body":null}`,
189+
WithError: nil,
190+
WithRequest: manualImportReq,
191+
ExpectedRequest: expectedBody,
192+
ExpectedMethod: "POST",
193+
WithResponse: &lidarr.CommandResponse{
194+
ID: 99,
195+
Name: "ManualImport",
196+
CommandName: "ManualImport",
197+
Message: "",
198+
Priority: "normal",
199+
Status: "queued",
200+
Queued: somedate,
201+
Started: somedate,
202+
Ended: somedate,
203+
StateChangeTime: somedate,
204+
LastExecutionTime: somedate,
205+
Duration: "",
206+
Trigger: "manual",
207+
SendUpdatesToClient: true,
208+
UpdateScheduledTask: false,
209+
Body: nil,
210+
},
211+
},
212+
{
213+
Name: "404",
214+
ExpectedPath: path.Join("/", starr.API, lidarr.APIver, "command"),
215+
ResponseStatus: http.StatusNotFound,
216+
ResponseBody: `{"message": "NotFound"}`,
217+
WithError: &starr.ReqError{Code: http.StatusNotFound},
218+
WithRequest: manualImportReq,
219+
ExpectedRequest: expectedBody,
220+
ExpectedMethod: "POST",
221+
WithResponse: (*lidarr.CommandResponse)(nil),
222+
},
223+
}
224+
225+
for _, test := range tests {
226+
t.Run(test.Name, func(t *testing.T) {
227+
t.Parallel()
228+
mockServer := test.GetMockServer(t)
229+
client := lidarr.New(starr.New("mockAPIkey", mockServer.URL, 0))
230+
req := test.WithRequest.(*lidarr.ManualImportCommandRequest)
231+
// Shallow copy so in-place mutation (Name/ImportMode defaults) doesn't affect other tests
232+
reqCopy := *req
233+
reqCopy.Files = req.Files
234+
output, err := client.SendManualImportCommandContext(context.Background(), &reqCopy)
235+
require.ErrorIs(t, err, test.WithError, "error is not the same as expected")
236+
assert.EqualValues(t, test.WithResponse, output, "response is not the same as expected")
237+
})
238+
}
239+
}

lidarr/manualimport.go

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,26 +69,88 @@ type ManualImportParams struct {
6969
FilterExistingFiles bool
7070
}
7171

72-
// ManualImport initiates a manual import (GET).
73-
func (l *Lidarr) ManualImport(params *ManualImportParams) (*ManualImportOutput, error) {
72+
// ManualImport returns the list of files available for manual import in the given folder (GET).
73+
// The Lidarr API returns an array of manual import items.
74+
func (l *Lidarr) ManualImport(params *ManualImportParams) ([]*ManualImportOutput, error) {
7475
return l.ManualImportContext(context.Background(), params)
7576
}
7677

77-
// ManualImportContext initiates a manual import (GET).
78-
func (l *Lidarr) ManualImportContext(ctx context.Context, params *ManualImportParams) (*ManualImportOutput, error) {
78+
// ManualImportContext returns the list of files available for manual import in the given folder (GET).
79+
func (l *Lidarr) ManualImportContext(ctx context.Context, params *ManualImportParams) ([]*ManualImportOutput, error) {
7980
req := starr.Request{URI: bpManualImport, Query: make(url.Values)}
8081
req.Query.Add("folder", params.Folder)
8182
req.Query.Add("downloadId", params.DownloadID)
8283
req.Query.Add("artistId", starr.Str(params.ArtistID))
8384
req.Query.Add("replaceExistingFiles", starr.Str(params.ReplaceExistingFiles))
8485
req.Query.Add("filterExistingFiles", starr.Str(params.FilterExistingFiles))
8586

86-
var output ManualImportOutput
87+
var output []*ManualImportOutput
8788
if err := l.GetInto(ctx, req, &output); err != nil {
8889
return nil, fmt.Errorf("api.Get(%s): %w", &req, err)
8990
}
9091

91-
return &output, nil
92+
return output, nil
93+
}
94+
95+
// ManualImportCommandFromOutputs builds a ManualImportCommandRequest from the GET manualimport response.
96+
// Use with SendManualImportCommand to trigger import of the listed files (e.g. after FLAC+CUE split).
97+
func ManualImportCommandFromOutputs(outputs []*ManualImportOutput, replaceExisting bool) *ManualImportCommandRequest {
98+
if len(outputs) == 0 {
99+
return nil
100+
}
101+
102+
files := make([]*ManualImportFile, 0, len(outputs))
103+
104+
for _, output := range outputs {
105+
if output == nil {
106+
continue
107+
}
108+
109+
artistID := int64(0)
110+
if output.Artist != nil {
111+
artistID = output.Artist.ID
112+
}
113+
114+
albumID := int64(0)
115+
if output.Album != nil {
116+
albumID = output.Album.ID
117+
}
118+
119+
trackIDs := make([]int64, 0, len(output.Tracks))
120+
for _, t := range output.Tracks {
121+
if t != nil {
122+
trackIDs = append(trackIDs, t.ID)
123+
}
124+
}
125+
126+
quality := output.Quality
127+
if quality == nil {
128+
quality = &starr.Quality{}
129+
}
130+
131+
files = append(files, &ManualImportFile{
132+
Path: output.Path,
133+
ArtistID: artistID,
134+
AlbumID: albumID,
135+
AlbumReleaseID: output.AlbumReleaseID,
136+
TrackIDs: trackIDs,
137+
Quality: quality,
138+
IndexerFlags: 0,
139+
DownloadID: output.DownloadID,
140+
DisableReleaseSwitching: output.DisableReleaseSwitching,
141+
})
142+
}
143+
144+
if len(files) == 0 {
145+
return nil
146+
}
147+
148+
return &ManualImportCommandRequest{
149+
Name: "ManualImport",
150+
Files: files,
151+
ImportMode: "auto",
152+
ReplaceExistingFiles: replaceExisting,
153+
}
92154
}
93155

94156
// ManualImportReprocess reprocesses a manual import (POST).

0 commit comments

Comments
 (0)