From c01eff0975d3276f33548fb95438adca6c6c93d8 Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Tue, 22 Sep 2026 21:13:14 -0700 Subject: [PATCH 1/5] Unify the test workflow and pin action digests --- .github/renovate.json | 23 ++++++-------- .github/workflows/codetests.yml | 33 ++++++++++--------- cameras_test.go | 18 +++++------ files.go | 4 +-- schedules_test.go | 6 ++-- securityspy.go | 4 +-- securityspy_test.go | 56 ++++++++++++++++----------------- server/server.go | 4 +-- server/server_test.go | 28 ++++++++--------- 9 files changed, 89 insertions(+), 87 deletions(-) diff --git a/.github/renovate.json b/.github/renovate.json index fb0b963..3b65103 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -1,25 +1,22 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": ["config:base", "default:automergeDigest"], + "extends": [ + "config:base", + "default:automergeDigest", + "helpers:pinGitHubActionDigestsToSemver" + ], + "minimumReleaseAge": "7 days", + "internalChecksFilter": "strict", "packageRules": [ { - "description": "Group Go stdlib / toolchain updates and automerge when tests pass", + "description": "Automerge non-major Go module updates when checks pass", "matchManagers": ["gomod"], - "matchDepNames": ["go"], - "matchDepTypes": ["golang", "toolchain"], - "groupName": "go", - "rangeStrategy": "bump", + "matchUpdateTypes": ["minor", "patch", "pin", "digest"], "automerge": true }, { - "description": "Group GitHub Actions updates and automerge when tests pass", + "description": "Automerge GitHub Actions updates, including majors, when checks pass", "matchManagers": ["github-actions"], - "groupName": "github-actions", - "automerge": true - }, - { - "matchDatasources": ["go", "github-releases", "github-tags"], - "matchUpdateTypes": ["minor", "patch", "pin", "digest"], "automerge": true } ] diff --git a/.github/workflows/codetests.yml b/.github/workflows/codetests.yml index 50da11a..f8e454c 100644 --- a/.github/workflows/codetests.yml +++ b/.github/workflows/codetests.yml @@ -3,12 +3,17 @@ on: pull_request: branches: - main - push: - branches: - - main permissions: contents: read jobs: + codespell: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: codespell-project/actions-codespell@e3258cca84ce02b0bb36a3d41f2c18f719a0cc1a # v2.4.3 + with: + skip: .git,go.sum + ignore_words_list: Medias # Runs `go test` against 3 operating systems. gotest: strategy: @@ -16,12 +21,12 @@ jobs: os: [macos, windows, ubuntu] runs-on: ${{ matrix.os }}-latest steps: - - uses: actions/checkout@v7 - - uses: actions/setup-go@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version: stable - name: go-test - run: go test -race '-test.v' -covermode=atomic ./... + run: go test -race -covermode=atomic '-test.v' ./... # Runs golangci-lint on macos against freebsd and macos. golangci-darwin: strategy: @@ -32,14 +37,14 @@ jobs: env: GOOS: ${{ matrix.os }} steps: - - uses: actions/setup-go@v7 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version: stable - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: golangci-lint - uses: golangci/golangci-lint-action@v9 + uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0 with: - version: v2.12 + version: v2.13 # Runs golangci-lint on linux against linux and windows. golangci-linux: strategy: @@ -50,11 +55,11 @@ jobs: env: GOOS: ${{ matrix.os }} steps: - - uses: actions/setup-go@v7 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version: stable - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: golangci-lint - uses: golangci/golangci-lint-action@v9 + uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0 with: - version: v2.12 + version: v2.13 diff --git a/cameras_test.go b/cameras_test.go index ef7f9b5..18f90d3 100644 --- a/cameras_test.go +++ b/cameras_test.go @@ -11,13 +11,13 @@ import ( func TestUnmarshalXMLCameraSchedule(t *testing.T) { t.Parallel() - asert := assert.New(t) + check := assert.New(t) var s securityspy.CameraSchedule err := xml.Unmarshal([]byte("3"), &s) require.NoError(t, err, "valid data must not produce an error") - asert.Equal(3, s.ID, "the data was not unmarshalled properly") + check.Equal(3, s.ID, "the data was not unmarshalled properly") } // SS 5.5 keeps v5 width/height tags but also emits video-format (a v6-era field). @@ -54,40 +54,40 @@ func TestUnmarshalXMLCameraSS55WithVideoFormat(t *testing.T) { func TestAll(t *testing.T) { t.Parallel() - asert := assert.New(t) + check := assert.New(t) secspyServer, _, _ := testServerWithCamera(t) cams := secspyServer.Cameras.All() - asert.Len(cams, 2, "the data contains two cameras, two cameras must be returned") + check.Len(cams, 2, "the data contains two cameras, two cameras must be returned") } func TestByNum(t *testing.T) { t.Parallel() - asert := assert.New(t) + check := assert.New(t) secspyServer, _, _ := testServerWithCamera(t) cam := secspyServer.Cameras.ByNum(2) - asert.Equal("Porch", cam.Name, "camera 2 is Porch in the v6 test data") + check.Equal("Porch", cam.Name, "camera 2 is Porch in the v6 test data") require.Nil(t, secspyServer.Cameras.ByNum(99), "a non-existent camera must return nil") } func TestByName(t *testing.T) { t.Parallel() - asert := assert.New(t) + check := assert.New(t) secspyServer, _, _ := testServerWithCamera(t) cam := secspyServer.Cameras.ByName("Porch") - asert.Equal(2, cam.Number, "camera 2 is Porch in the v6 test data") + check.Equal(2, cam.Number, "camera 2 is Porch in the v6 test data") require.Nil(t, secspyServer.Cameras.ByName("not here"), "a non-existent camera must return nil") cam = secspyServer.Cameras.ByName("porch2") require.Nil(t, cam, "there is no camera named porch2") cam = secspyServer.Cameras.ByName("porch") - asert.Equal(2, cam.Number, "camera 2 is Porch in the v6 test data") + check.Equal(2, cam.Number, "camera 2 is Porch in the v6 test data") require.Nil(t, secspyServer.Cameras.ByName("not here"), "a non-existent camera must return nil") } diff --git a/files.go b/files.go index e85d296..130f2d3 100644 --- a/files.go +++ b/files.go @@ -50,7 +50,7 @@ type fileFeed struct { Entries []*File `xml:"entry"` // List of File pointers } -// File represents a saved media file. This is all the data retreived from +// File represents a saved media file. This is all the data retrieved from // the ++download method for a particular file. Contains a camera interface // for the camera that created the file. All of the Files type methods return this type. type File struct { @@ -214,7 +214,7 @@ func (f *Files) getFiles(cameraNums []int, start, end time.Time, fileTypes, cont return entries, nil } -// makeFilesParams makes the url Values for a file retreival. +// makeFilesParams makes the url Values for a file retrieval. func makeFilesParams(cameraNums []int, from, to time.Time, fileTypes, continuation string) url.Values { params := make(url.Values) params.Set("results", "1000") diff --git a/schedules_test.go b/schedules_test.go index f37b011..69ebb0e 100644 --- a/schedules_test.go +++ b/schedules_test.go @@ -24,14 +24,14 @@ func TestSetSchedulePreset(t *testing.T) { func TestUnmarshalXMLscheduleContainer(t *testing.T) { t.Parallel() - asert := assert.New(t) + check := assert.New(t) var schedule securityspy.ScheduleContainer err := xml.Unmarshal([]byte(testScheduleList), &schedule) require.NoError(t, err, "valid data must not produce an error") - asert.Equal("Armed 24/7", schedule[1], "the scheduleContainer data did not unmarshal properly") - asert.Len(schedule, 6) + check.Equal("Armed 24/7", schedule[1], "the scheduleContainer data did not unmarshal properly") + check.Len(schedule, 6) err = xml.Unmarshal([]byte(""), &schedule) require.Error(t, err, "invalid data must produce an error") diff --git a/securityspy.go b/securityspy.go index 69626bd..d798681 100644 --- a/securityspy.go +++ b/securityspy.go @@ -21,9 +21,9 @@ func New(c *server.Config) (*Server, error) { return s, s.RefreshContext(context.Background()) //nolint:gocritic } -// NewMust returns an iterface to interact with SecuritySpy. +// NewMust returns an interface to interact with SecuritySpy. // This does not attempt to connect to SecuritySpy first. -// You must call s.Refresh() before attempting to access other datas. +// You must call s.Refresh() before attempting to access other data. func NewMust(config *server.Config) *Server { if !strings.HasSuffix(config.URL, "/") { config.URL += "/" diff --git a/securityspy_test.go b/securityspy_test.go index a9fda0a..3c80c88 100644 --- a/securityspy_test.go +++ b/securityspy_test.go @@ -23,14 +23,14 @@ const ( func TestGetServer(t *testing.T) { t.Parallel() - asert := assert.New(t) + check := assert.New(t) URL := "http://127.0.0.1:5678" user := "user123" pass := "pass456" secspyServer, err := securityspy.New(&server.Config{Username: user, Password: pass, URL: URL, VerifySSL: true}) require.Error(t, err, "there is no server at the address provided so an error must exist") - asert.NotNil(secspyServer, "server must not be nil. even wiuth an error it must be returned") + check.NotNil(secspyServer, "server must not be nil. even wiuth an error it must be returned") if !strings.Contains(err.Error(), "target machine actively refused it") && !strings.Contains(err.Error(), "connection refused") { @@ -62,26 +62,26 @@ func TestRefresh(t *testing.T) { })) defer fakeServer.Close() - asert := assert.New(t) + check := assert.New(t) secspyServer := securityspy.NewMust( &server.Config{Username: userStr, Password: passStr, URL: fakeServer.URL + "/", VerifySSL: false}) require.NoError(t, secspyServer.Refresh(), "an error must not be returned while testing with valid XML") // Make sure Refresh() did all the things it is supposed to do. - asert.WithinDuration(time.Now(), secspyServer.Info.Refreshed, time.Second, + check.WithinDuration(time.Now(), secspyServer.Info.Refreshed, time.Second, "Refreshed field must be updated by Refresh() method") // Test that the data was unmarshalled properly. // These tests assume the test data does not change. - asert.Equal("SecuritySpy", secspyServer.Info.Name, "the server's name was not properly unmarshalled") - asert.Equal("2019-02-10T15:53:23", secspyServer.Info.CurrentTime.Format("2006-01-02T15:04:05"), + check.Equal("SecuritySpy", secspyServer.Info.Name, "the server's name was not properly unmarshalled") + check.Equal("2019-02-10T15:53:23", secspyServer.Info.CurrentTime.Format("2006-01-02T15:04:05"), "the server's current time was not properly unmarshalled") - asert.Equal(2304, secspyServer.Cameras.ByNum(1).Width, "camera info was not properly unmarshalled") - asert.Equal("Road", secspyServer.Cameras.ByNum(2).Name, "camera info was not properly unmarshalled") - asert.Equal("Unarmed 24/7", secspyServer.Info.ServerSchedules[0], "schedule info was not properly unmarshalled") - asert.Equal("None", secspyServer.Info.ScheduleOverrides[0], "schedule override info was not properly unmarshalled") - asert.Equal("MyFirstPreset", secspyServer.Info.SchedulePresets[1930238093], + check.Equal(2304, secspyServer.Cameras.ByNum(1).Width, "camera info was not properly unmarshalled") + check.Equal("Road", secspyServer.Cameras.ByNum(2).Name, "camera info was not properly unmarshalled") + check.Equal("Unarmed 24/7", secspyServer.Info.ServerSchedules[0], "schedule info was not properly unmarshalled") + check.Equal("None", secspyServer.Info.ScheduleOverrides[0], "schedule override info was not properly unmarshalled") + check.Equal("MyFirstPreset", secspyServer.Info.SchedulePresets[1930238093], "schedule preset info was not properly unmarshalled") // second call returns an HTTP error response. @@ -111,14 +111,14 @@ func TestGetSounds(t *testing.T) { //nolint:dupl // it just looks like a duplica })) defer fakeServer.Close() - asert := assert.New(t) + check := assert.New(t) secspyServer := securityspy.NewMust( &server.Config{Username: userStr, Password: passStr, URL: fakeServer.URL + "/", VerifySSL: false}) sounds, err := secspyServer.GetSounds() require.NoError(t, err, "the method must not return an error when given valid XML to unmarshal") - asert.Len(sounds, 20, "all 20 sounds must exist in the slice") - asert.Equal("Beeps.aif", sounds[0], "the sound files were not properly unmarhsalled") + check.Len(sounds, 20, "all 20 sounds must exist in the slice") + check.Equal("Beeps.aif", sounds[0], "the sound files were not properly unmarhsalled") _, err = secspyServer.GetSounds() require.Error(t, err) @@ -147,14 +147,14 @@ func TestGetScripts(t *testing.T) { //nolint:dupl // it just looks like a duplic })) defer fakeServer.Close() - asert := assert.New(t) + check := assert.New(t) secspyServer := securityspy.NewMust( &server.Config{Username: userStr, Password: passStr, URL: fakeServer.URL + "/", VerifySSL: false}) scripts, err := secspyServer.GetScripts() require.NoError(t, err, "the method must not return an error when given valid XML to unmarshal") - asert.Len(scripts, 16, "all 16 scripts must exist in the slice") - asert.Equal("Web-i Activate Relay 1.scpt", scripts[0], "the script files were not properly unmarhsalled") + check.Len(scripts, 16, "all 16 scripts must exist in the slice") + check.Equal("Web-i Activate Relay 1.scpt", scripts[0], "the script files were not properly unmarhsalled") _, err = secspyServer.GetScripts() require.Error(t, err) @@ -163,7 +163,7 @@ func TestGetScripts(t *testing.T) { //nolint:dupl // it just looks like a duplic func TestUnmarshalXMLYesNoBool(t *testing.T) { t.Parallel() - asert := assert.New(t) + check := assert.New(t) good := []string{"true", "yes", "1", "armed", "active", "enabled"} fail := []string{"anything", "else", "returns", "false", "including", "no", "0", "disarmed", "inactive", "disabled"} @@ -171,38 +171,38 @@ func TestUnmarshalXMLYesNoBool(t *testing.T) { for _, val := range good { require.NoError(t, xml.Unmarshal([]byte(""+val+""), &bit), "unmarshalling must not produce an error") - asert.True(bit.Val, "the value must unmarshal to true") - asert.Equal(val, bit.Txt, "the value was not unmarshalled correctly") + check.True(bit.Val, "the value must unmarshal to true") + check.Equal(val, bit.Txt, "the value was not unmarshalled correctly") } for _, val := range fail { require.NoError(t, xml.Unmarshal([]byte(""+val+""), &bit), "unmarshalling must not produce an error") - asert.False(bit.Val, "the value must unmarshal to false") - asert.Equal(val, bit.Txt, "the value was not unmarshalled correctly") + check.False(bit.Val, "the value must unmarshal to false") + check.Equal(val, bit.Txt, "the value was not unmarshalled correctly") } } func TestUnmarshalXMLDuration(t *testing.T) { t.Parallel() - asert := assert.New(t) + check := assert.New(t) good := []string{"1", "20", "300", "4000", "50000", "666666"} var bit securityspy.Duration for _, val := range good { require.NoError(t, xml.Unmarshal([]byte(""+val+""), &bit), "unmarshalling must not produce an error") - asert.Equal(val, bit.Val, "the value was not unmarshalled correctly") + check.Equal(val, bit.Val, "the value was not unmarshalled correctly") num, err := strconv.ParseFloat(val, 64) require.NoError(t, err, "must not be an error parsing test numbers") - asert.InDelta(num, bit.Seconds(), 0.000001, "the value was not unmarshalled correctly") - asert.Equal(val, bit.Val, "the value was not unmarshalled correctly") + check.InDelta(num, bit.Seconds(), 0.000001, "the value was not unmarshalled correctly") + check.Equal(val, bit.Val, "the value was not unmarshalled correctly") } // Test empty value. require.NoError(t, xml.Unmarshal([]byte(""), &bit), "unmarshalling must not produce an error") - asert.Empty(bit.Val, "the value was not unmarshalled correctly") - asert.Equal(int64(-1), bit.Nanoseconds(), "an empty value must produce -1 nano second.") + check.Empty(bit.Val, "the value was not unmarshalled correctly") + check.Equal(int64(-1), bit.Nanoseconds(), "an empty value must produce -1 nano second.") } func TestRefreshHandlesNilPTZAndMissingSchedules(t *testing.T) { diff --git a/server/server.go b/server/server.go index cbc261b..dfc1da5 100644 --- a/server/server.go +++ b/server/server.go @@ -251,7 +251,7 @@ func (s *Config) GetXMLContext(ctx context.Context, apiPath string, params url.V return nil } -// SimpleReq performes HTTP req, checks for OK at end of output. +// SimpleReq performs HTTP req, checks for OK at end of output. func (s *Config) SimpleReq(apiURI string, params url.Values, cameraNum int) error { ctx, cancel := context.WithTimeout(context.Background(), s.TimeoutDur()) defer cancel() @@ -259,7 +259,7 @@ func (s *Config) SimpleReq(apiURI string, params url.Values, cameraNum int) erro return s.SimpleReqContext(ctx, apiURI, params, cameraNum) } -// SimpleReqContext performes HTTP req, checks for OK at end of output. +// SimpleReqContext performs HTTP req, checks for OK at end of output. func (s *Config) SimpleReqContext(ctx context.Context, apiURI string, params url.Values, cameraNum int) error { if cameraNum >= 0 { params.Set("cameraNum", strconv.Itoa(cameraNum)) diff --git a/server/server_test.go b/server/server_test.go index 07c3b2d..e2dbe78 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -25,7 +25,7 @@ const ( func TestGet(t *testing.T) { t.Parallel() - asert := assert.New(t) + check := assert.New(t) config := &server.Config{ Username: userStr, Password: passStr, @@ -34,13 +34,13 @@ func TestGet(t *testing.T) { Timeout: server.Duration{time.Second}, } handler := http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - asert.Equal("xml", req.URL.Query().Get("format"), "format parameter was not added") - asert.Equal(config.Password, req.URL.Query().Get("auth"), "auth parameter was not added") - asert.Equal("application/xml", req.Header.Get("Accept"), "accept header is not correct") + check.Equal("xml", req.URL.Query().Get("format"), "format parameter was not added") + check.Equal(config.Password, req.URL.Query().Get("auth"), "auth parameter was not added") + check.Equal("application/xml", req.Header.Get("Accept"), "accept header is not correct") _, err := resp.Write([]byte("request OK")) - asert.NoError(err, "the fake server must return an error writing to the client") - asert.Equal(config.URL, "http://"+req.Host+"/", "the host was not set correctly in the request") + check.NoError(err, "the fake server must return an error writing to the client") + check.Equal(config.URL, "http://"+req.Host+"/", "the host was not set correctly in the request") }) httpClient, fakeServer := testingHTTPClient(handler) @@ -53,17 +53,17 @@ func TestGet(t *testing.T) { if err == nil { defer resp.Body.Close() - asert.Equal(http.StatusOK, resp.StatusCode, "the server must return a 200 response code") + check.Equal(http.StatusOK, resp.StatusCode, "the server must return a 200 response code") body, err := io.ReadAll(resp.Body) require.NoError(t, err, "must not be an error reading the response body") - asert.Equal("request OK", string(body), "wrong data was returned from the server") + check.Equal("request OK", string(body), "wrong data was returned from the server") } } func TestPostAudioContentType(t *testing.T) { t.Parallel() - asert := assert.New(t) + check := assert.New(t) config := &server.Config{ Username: userStr, Password: passStr, @@ -86,7 +86,7 @@ func TestPostAudioContentType(t *testing.T) { } _, err := resp.Write([]byte("OK")) - asert.NoError(err) + check.NoError(err) }) httpClient, fakeServer := testingHTTPClient(handler) @@ -96,14 +96,14 @@ func TestPostAudioContentType(t *testing.T) { body, err := config.Post("++audio", nil, io.NopCloser(strings.NewReader("audio-data"))) require.NoError(t, err) - asert.Equal("OK", string(body)) + check.Equal("OK", string(body)) body, err = config.Post("++other", nil, io.NopCloser(strings.NewReader("other-data"))) require.NoError(t, err) - asert.Equal("OK", string(body)) + check.Equal("OK", string(body)) - asert.Equal("audio/g711-ulaw", audioHeader) - asert.Empty(otherHeader) + check.Equal("audio/g711-ulaw", audioHeader) + check.Empty(otherHeader) } func TestGetXMLStatusAndDecodeErrors(t *testing.T) { From dc2916bfe78f183788a8d50eccd54c8eda9c89a9 Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Tue, 22 Sep 2026 21:26:40 -0700 Subject: [PATCH 2/5] Use the config:recommended Renovate preset. --- .github/renovate.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/renovate.json b/.github/renovate.json index 3b65103..aee303c 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -1,7 +1,7 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ - "config:base", + "config:recommended", "default:automergeDigest", "helpers:pinGitHubActionDigestsToSemver" ], From 6bc4c172329199e2160d1ef0d4e545e6584fa112 Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Wed, 23 Sep 2026 00:14:03 -0700 Subject: [PATCH 3/5] Tighten the securityspy lint config and fix the findings. Drop the disables that were quiet, keep exhaustruct_v5 off for the large camera structs, and treat a default case as exhaustive. Co-authored-by: Cursor --- .golangci.yml | 29 ++++++---------- cameras.go | 23 ++++++++----- cameras_types.go | 7 ++-- events.go | 12 ++++--- files.go | 33 +++++++++++++----- internal/rtspclip/clip.go | 61 +++++++++++++++++++++++----------- internal/rtspclip/clip_test.go | 2 -- ptz.go | 30 ++++++++--------- schedules.go | 6 ++-- securityspy.go | 6 ++-- securityspy_test.go | 3 ++ server/server.go | 11 +++--- server/server_test.go | 2 +- settings.go | 49 +++++++++++++++++++-------- 14 files changed, 172 insertions(+), 102 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 8753653..2e30ef0 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,30 +1,23 @@ -version: '2' +version: "2" linters: default: all disable: - - exhaustruct - - depguard - - wsl - - funcorder - - noinlineerr - - exhaustive - - nlreturn - - gomodguard - - goconst + - wsl # deprecated. + - exhaustruct # deprecated. + - exhaustruct_v5 # camera and XML structs are filled a field at a time. + - gomodguard # deprecated. + - depguard # not useful. settings: + exhaustive: + default-signifies-exhaustive: true gocritic: enable-all: true + disabled-checks: + - whyNoLint + - commentedOutCode settings: unnamedResult: checkExported: true - errcheck: - check-type-assertions: true - check-blank: false - disable-default-exclusions: false - exclude-functions: - - (*os.File).Close - - (io.Closer).Close - issues: max-issues-per-linter: 0 max-same-issues: 0 diff --git a/cameras.go b/cameras.go index 89a2e42..5275ae1 100644 --- a/cameras.go +++ b/cameras.go @@ -88,7 +88,8 @@ func (c *Camera) StreamVideo(ops *VidOps, length time.Duration, maxsize int64) ( // UseHTTP is not supported (returns ErrHTTPVideoUnsupported). // Set VidOps.VCodec to "h265" for HEVC cameras (see PreferredVCodec). func (c *Camera) SaveVideo(ops *VidOps, length time.Duration, maxsize int64, outputFile string) error { - if _, err := os.Stat(outputFile); !os.IsNotExist(err) { + _, statErr := os.Stat(outputFile) + if !os.IsNotExist(statErr) { return ErrPathExists } @@ -351,7 +352,8 @@ func (c *Camera) ToggleContinuous(arm CameraArmMode) error { params := make(url.Values) params.Set("arm", string(arm)) - if err := c.server.SimpleReq("++ssControlContinuous", params, c.Number); err != nil { + err := c.server.SimpleReq("++ssControlContinuous", params, c.Number) + if err != nil { if errors.Is(err, server.ErrNotFound) { return ErrUnsupported } @@ -367,7 +369,8 @@ func (c *Camera) ToggleMotion(arm CameraArmMode) error { params := make(url.Values) params.Set("arm", string(arm)) - if err := c.server.SimpleReq("++ssControlMotionCapture", params, c.Number); err != nil { + err := c.server.SimpleReq("++ssControlMotionCapture", params, c.Number) + if err != nil { return fmt.Errorf("request failed: %w", err) } @@ -379,7 +382,8 @@ func (c *Camera) ToggleActions(arm CameraArmMode) error { params := make(url.Values) params.Set("arm", string(arm)) - if err := c.server.SimpleReq("++ssControlActions", params, c.Number); err != nil { + err := c.server.SimpleReq("++ssControlActions", params, c.Number) + if err != nil { return fmt.Errorf("request failed: %w", err) } @@ -389,7 +393,8 @@ func (c *Camera) ToggleActions(arm CameraArmMode) error { // TriggerMotion sets a camera as currently seeing motion. // Other actions likely occur because of this! func (c *Camera) TriggerMotion() error { - if err := c.server.SimpleReq("++triggermd", make(url.Values), c.Number); err != nil { + err := c.server.SimpleReq("++triggermd", make(url.Values), c.Number) + if err != nil { return fmt.Errorf("request failed: %w", err) } @@ -402,7 +407,7 @@ func (c *Camera) Modes() (*CameraModes, error) { if err != nil { return nil, fmt.Errorf("getting camera modes: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("%w: %s", ErrCameraModesStatus, resp.Status) @@ -497,7 +502,8 @@ func (c *Camera) SetSchedule(mode CameraMode, scheduleID int) error { params.Set("mode", string(mode)) params.Set("id", strconv.Itoa(scheduleID)) - if err := c.server.SimpleReq("++ssSetSchedule", params, c.Number); err != nil { + err := c.server.SimpleReq("++ssSetSchedule", params, c.Number) + if err != nil { return fmt.Errorf("request failed: %w", err) } @@ -512,7 +518,8 @@ func (c *Camera) SetScheduleOverride(mode CameraMode, overrideID int) error { params.Set("mode", string(mode)) params.Set("id", strconv.Itoa(overrideID)) - if err := c.server.SimpleReq("++ssSetOverride", params, c.Number); err != nil { + err := c.server.SimpleReq("++ssSetOverride", params, c.Number) + if err != nil { return fmt.Errorf("request failed: %w", err) } diff --git a/cameras_types.go b/cameras_types.go index fdadb05..198878b 100644 --- a/cameras_types.go +++ b/cameras_types.go @@ -105,7 +105,8 @@ type CameraSchedule struct { // UnmarshalXML stores a schedule ID into a CameraSchedule type. // This isn't a method you should ever call directly; it is only used during data initialization. func (bit *CameraSchedule) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { - if err := d.DecodeElement(&bit.ID, &start); err != nil { + err := d.DecodeElement(&bit.ID, &start) + if err != nil { return fmt.Errorf("decoding xml: %w", err) } @@ -340,7 +341,9 @@ type cameraXML struct { //nolint:funlen // dual schema mapping func (c *Camera) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var raw cameraXML - if err := d.DecodeElement(&raw, &start); err != nil { + + err := d.DecodeElement(&raw, &start) + if err != nil { return fmt.Errorf("decoding camera xml: %w", err) } diff --git a/events.go b/events.go index 756d88c..3740381 100644 --- a/events.go +++ b/events.go @@ -325,7 +325,8 @@ func (e *Events) eventStreamSelector(ctx context.Context, refreshOnConfigChange } func (e *Events) serverRefresh(ctx context.Context) { - if err := e.server.RefreshContext(ctx); err != nil { + err := e.server.RefreshContext(ctx) + if err != nil { e.custom(EventWatcherRefreshFail, -9997, -1, err.Error()) return @@ -380,13 +381,15 @@ func (e *Events) UnmarshalEvent(text string) *Event { eventTime = fmt.Sprintf("%v%+03.0f", parts[0], gmtOffset) //nolint:gosmopolitan // The event stream uses the system's local time. - if newEvent.When, err = time.ParseInLocation(EventTimeFormat+"-07", eventTime, time.Local); err != nil { + newEvent.When, err = time.ParseInLocation(EventTimeFormat+"-07", eventTime, time.Local) + if err != nil { newEvent.When = time.Now() newEvent.Errors = append(newEvent.Errors, ErrDateParseFail) } // Parse the ID - if newEvent.ID, err = strconv.Atoi(parts[1]); err != nil { + newEvent.ID, err = strconv.Atoi(parts[1]) + if err != nil { newEvent.ID = BadID newEvent.Errors = append(newEvent.Errors, ErrIDParseFail) } @@ -395,7 +398,8 @@ func (e *Events) UnmarshalEvent(text string) *Event { parts[2] = strings.TrimPrefix(parts[2], "CAM") if cams := e.server.GetCameras(); parts[2] != "X" && cams != nil { - if cameraNum, err := strconv.Atoi(parts[2]); err != nil { + cameraNum, err := strconv.Atoi(parts[2]) + if err != nil { newEvent.Errors = append(newEvent.Errors, ErrCAMParseFail) } else if newEvent.Camera = cams.ByNum(cameraNum); newEvent.Camera == nil { newEvent.Errors = append(newEvent.Errors, ErrCAMMissing) diff --git a/files.go b/files.go index 130f2d3..efde94f 100644 --- a/files.go +++ b/files.go @@ -110,15 +110,28 @@ func (f *Files) GetFile(name string) (*File, error) { } cams := f.server.GetCameras() - if fileExtSplit := strings.Split(name, "."); len(fileExtSplit) != fileParts { + fileExtSplit := strings.Split(name, ".") + if len(fileExtSplit) != fileParts { return file, ErrInvalidName - } else if nameDateSplit := strings.Split(fileExtSplit[0], " "); len(fileExtSplit) < fileParts { + } + + nameDateSplit := strings.Split(fileExtSplit[0], " ") + if len(fileExtSplit) < fileParts { return file, ErrInvalidName - } else if file.Updated, err = time.Parse(FileDateFormat, nameDateSplit[0]); err != nil { + } + + file.Updated, err = time.Parse(FileDateFormat, nameDateSplit[0]) + if err != nil { return file, ErrInvalidName - } else if file.Camera = cams.ByName(nameDateSplit[len(nameDateSplit)-1]); file.Camera == nil { + } + + file.Camera = cams.ByName(nameDateSplit[len(nameDateSplit)-1]) + if file.Camera == nil { return file, ErrCAMMissing - } else if file.Link.Type = "video/quicktime"; fileExtSplit[1] == "jpg" { + } + + file.Link.Type = "video/quicktime" + if fileExtSplit[1] == "jpg" { file.Link.Type = "image/jpeg" } @@ -134,7 +147,8 @@ func (f *Files) GetFile(name string) (*File, error) { // Save downloads a saved media File from SecuritySpy and saves it to a local file. // Returns an error if path exists. func (f *File) Save(path string) (int64, error) { - if _, err := os.Stat(path); !os.IsNotExist(err) { + _, statErr := os.Stat(path) + if !os.IsNotExist(statErr) { return 0, ErrPathExists } @@ -142,13 +156,13 @@ func (f *File) Save(path string) (int64, error) { if err != nil { return 0, err } - defer body.Close() + defer func() { _ = body.Close() }() newFile, err := os.Create(path) //nolint:gosec // we are creating a file in a safe way. if err != nil { return 0, fmt.Errorf("os.Create(): %w", err) } - defer newFile.Close() + defer func() { _ = newFile.Close() }() size, err := io.Copy(newFile, body) if err != nil { @@ -187,7 +201,8 @@ func (f *Files) getFiles(cameraNums []int, start, end time.Time, fileTypes, cont params = makeFilesParams(cameraNums, start, end, fileTypes, continuation) ) - if err := f.server.GetXML("++download", params, &feed); err != nil { + err := f.server.GetXML("++download", params, &feed) + if err != nil { return nil, fmt.Errorf("getting download: %w", err) } diff --git a/internal/rtspclip/clip.go b/internal/rtspclip/clip.go index d8a8b81..3d3749a 100644 --- a/internal/rtspclip/clip.go +++ b/internal/rtspclip/clip.go @@ -89,7 +89,8 @@ func SaveMP4(ctx context.Context, rtspURL, path string, opts Options) (*Result, return nil, ErrBadPath } - if _, err := os.Stat(path); err == nil { + _, statErr := os.Stat(path) + if statErr == nil { return nil, os.ErrExist } @@ -98,7 +99,7 @@ func SaveMP4(ctx context.Context, rtspURL, path string, opts Options) (*Result, return nil, fmt.Errorf("create output: %w", err) } - defer file.Close() + defer func() { _ = file.Close() }() res, err := capture(ctx, rtspURL, opts, file, false) if err != nil { @@ -147,7 +148,8 @@ type cancelReadCloser struct { func (c *cancelReadCloser) Close() error { c.cancel() - if err := c.ReadCloser.Close(); err != nil { + err := c.ReadCloser.Close() + if err != nil { return fmt.Errorf("close stream: %w", err) } @@ -162,7 +164,8 @@ func capture(ctx context.Context, rtspURL string, opts Options, writer io.Writer client := newRTSPClient(parsed, opts.InsecureSkipVerify) - if err := client.Start(); err != nil { + err = client.Start() + if err != nil { return nil, fmt.Errorf("rtsp start: %w", err) } @@ -186,22 +189,26 @@ func capture(ctx context.Context, rtspURL string, opts Options, writer io.Writer state := &captureState{} stop := makeStopper(state, cancel, client) - if err := client.SetupAll(session.BaseURL, session.Medias); err != nil { + err = client.SetupAll(session.BaseURL, session.Medias) + if err != nil { return nil, fmt.Errorf("rtsp setup: %w", err) } attachVideo(client, tracks, mux, state, opts, stop) attachAudio(client, tracks, mux, state, opts, stop) - if _, err := client.Play(nil); err != nil { + _, err = client.Play(nil) + if err != nil { return nil, fmt.Errorf("rtsp play: %w", err) } - if err := waitCapture(captureCtx, client, state, stop); err != nil { + err = waitCapture(captureCtx, client, state, stop) + if err != nil { return nil, err } - if err := mux.close(); err != nil { + err = mux.close() + if err != nil { return nil, err } @@ -620,7 +627,8 @@ func (m *fmp4Muxer) writeVideo(accessUnit [][]byte, pts int64) (int, error) { return 0, nil } - if err := m.ensureVideoStarted(idr, pts); err != nil { + err := m.ensureVideoStarted(idr, pts) + if err != nil { return 0, err } @@ -674,7 +682,8 @@ func (m *fmp4Muxer) afterVideoSample() error { return nil } - if err := m.maybeInitProgressive(); err != nil { + err := m.maybeInitProgressive() + if err != nil { return err } @@ -787,8 +796,11 @@ func (m *fmp4Muxer) writeInit(includeAudio bool) error { if err != nil { return fmt.Errorf("hevc descriptor: %w", err) } - } else if err := init.Moov.Trak.SetAVCDescriptor("avc1", [][]byte{m.sps}, [][]byte{m.pps}, true); err != nil { - return fmt.Errorf("avc descriptor: %w", err) + } else { + err := init.Moov.Trak.SetAVCDescriptor("avc1", [][]byte{m.sps}, [][]byte{m.pps}, true) + if err != nil { + return fmt.Errorf("avc descriptor: %w", err) + } } if includeAudio && m.aacFormat != nil && m.aacFormat.Config != nil && m.audioRate > 0 { @@ -797,12 +809,14 @@ func (m *fmp4Muxer) writeInit(includeAudio bool) error { traks := init.Moov.Traks audioTrak := traks[len(traks)-1] - if err := setMPEG4AudioDescriptor(audioTrak, m.aacFormat.Config); err != nil { + err := setMPEG4AudioDescriptor(audioTrak, m.aacFormat.Config) + if err != nil { return err } } - if err := init.Encode(m.w); err != nil { + err := init.Encode(m.w) + if err != nil { return fmt.Errorf("write init: %w", err) } @@ -844,7 +858,9 @@ func (m *fmp4Muxer) maybeInitProgressive() error { } m.includeAudio = len(m.audioSamples) > 0 && m.aacFormat != nil && m.aacFormat.Config != nil - if err := m.writeInit(m.includeAudio); err != nil { + + err := m.writeInit(m.includeAudio) + if err != nil { return err } @@ -881,20 +897,23 @@ func (m *fmp4Muxer) flushFragment() error { seg.AddFragment(frag) for _, sample := range m.videoSamples { - if err := frag.AddFullSampleToTrack(sample, videoTrackID); err != nil { + err := frag.AddFullSampleToTrack(sample, videoTrackID) + if err != nil { return fmt.Errorf("add video sample: %w", err) } } if m.includeAudio { for _, sample := range m.audioSamples { - if err := frag.AddFullSampleToTrack(sample, audioTrackID); err != nil { + err := frag.AddFullSampleToTrack(sample, audioTrackID) + if err != nil { return fmt.Errorf("add audio sample: %w", err) } } } - if err := seg.Encode(m.w); err != nil { + err = seg.Encode(m.w) + if err != nil { return fmt.Errorf("encode segment: %w", err) } @@ -914,7 +933,8 @@ func (m *fmp4Muxer) close() error { } if m.progressive { - if err := m.maybeInitProgressive(); err != nil { + err := m.maybeInitProgressive() + if err != nil { return err } @@ -930,7 +950,8 @@ func (m *fmp4Muxer) close() error { m.hadAudio = true } - if err := m.writeInit(includeAudio); err != nil { + err := m.writeInit(includeAudio) + if err != nil { return err } diff --git a/internal/rtspclip/clip_test.go b/internal/rtspclip/clip_test.go index 350eea8..d14a47d 100644 --- a/internal/rtspclip/clip_test.go +++ b/internal/rtspclip/clip_test.go @@ -46,7 +46,6 @@ func TestMonoAAC64kASC(t *testing.T) { cfg := mpeg4audio.AudioSpecificConfig{ Type: mpeg4audio.ObjectTypeAACLC, SampleRate: 64000, - ChannelCount: 1, ChannelConfig: 1, } asc, err := cfg.Marshal() @@ -61,7 +60,6 @@ func TestHighSampleRateASCStillMarshals(t *testing.T) { cfg := mpeg4audio.AudioSpecificConfig{ Type: mpeg4audio.ObjectTypeAACLC, SampleRate: 96000, - ChannelCount: 2, ChannelConfig: 2, } asc, err := cfg.Marshal() diff --git a/ptz.go b/ptz.go index b419922..fad8ea9 100644 --- a/ptz.go +++ b/ptz.go @@ -198,24 +198,11 @@ func (z *PTZ) Stop() error { return z.ptzReq(ptzCommandStop) } -/* INTERFACE HELPER METHODS FOLLOW */ - -// ptzReq wraps all the ptz-specific calls. -func (z *PTZ) ptzReq(command ptzCommand) error { - params := make(url.Values) - params.Set("command", strconv.Itoa(int(command))) - - if err := z.camera.server.SimpleReq("++ptz/command", params, z.camera.Number); err != nil { - return fmt.Errorf("ptz failed: %w", err) - } - - return nil -} - // UnmarshalXML method converts ptzCapbilities bitmask from an XML payload into true/false abilities. // This isn't a method you should ever call directly; it is only used during data initialization. func (z *PTZ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { - if err := d.DecodeElement(&z.rawCaps, &start); err != nil { + err := d.DecodeElement(&z.rawCaps, &start) + if err != nil { return fmt.Errorf("ptz caps: %w", err) } @@ -228,3 +215,16 @@ func (z *PTZ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { return nil } + +// ptzReq wraps all the ptz-specific calls. +func (z *PTZ) ptzReq(command ptzCommand) error { + params := make(url.Values) + params.Set("command", strconv.Itoa(int(command))) + + err := z.camera.server.SimpleReq("++ptz/command", params, z.camera.Number) + if err != nil { + return fmt.Errorf("ptz failed: %w", err) + } + + return nil +} diff --git a/schedules.go b/schedules.go index a76c618..20bbd2d 100644 --- a/schedules.go +++ b/schedules.go @@ -46,7 +46,8 @@ func (m *ScheduleContainer) UnmarshalXML(decoder *xml.Decoder, start xml.StartEl switch xmlToken := token.(type) { case xml.StartElement: - if err = decoder.DecodeElement(&schedule, &xmlToken); err != nil { + err = decoder.DecodeElement(&schedule, &xmlToken) + if err != nil { return fmt.Errorf("XML decode: %w", err) } @@ -65,7 +66,8 @@ func (s *Server) SetSchedulePreset(presetID int) error { params := make(url.Values) params.Set("id", strconv.Itoa(presetID)) - if err := s.SimpleReq("++ssSetPreset", params, -1); err != nil { + err := s.SimpleReq("++ssSetPreset", params, -1) + if err != nil { return fmt.Errorf("http request: %w", err) } diff --git a/securityspy.go b/securityspy.go index d798681..9794603 100644 --- a/securityspy.go +++ b/securityspy.go @@ -169,7 +169,8 @@ func (s *Server) GetScripts() ([]string, error) { Names []string `xml:"name"` } - if err := s.GetXML("++scripts", nil, &val); err != nil { + err := s.GetXML("++scripts", nil, &val) + if err != nil { return nil, fmt.Errorf("getting scripts: %w", err) } @@ -183,7 +184,8 @@ func (s *Server) GetSounds() ([]string, error) { Names []string `xml:"name"` } - if err := s.GetXML("++sounds", nil, &val); err != nil { + err := s.GetXML("++sounds", nil, &val) + if err != nil { return nil, fmt.Errorf("getting sounds: %w", err) } diff --git a/securityspy_test.go b/securityspy_test.go index 3c80c88..42093f5 100644 --- a/securityspy_test.go +++ b/securityspy_test.go @@ -96,6 +96,7 @@ func TestGetSounds(t *testing.T) { //nolint:dupl // it just looks like a duplica fakeServer := httptest.NewServer(http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { if req.URL.Path != "/++sounds" { http.NotFound(resp, req) + return } @@ -132,6 +133,7 @@ func TestGetScripts(t *testing.T) { //nolint:dupl // it just looks like a duplic fakeServer := httptest.NewServer(http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { if req.URL.Path != "/++scripts" { http.NotFound(resp, req) + return } @@ -217,6 +219,7 @@ func TestRefreshHandlesNilPTZAndMissingSchedules(t *testing.T) { fakeServer := httptest.NewServer(http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { if req.URL.Path != "/++systemInfo" { http.NotFound(resp, req) + return } diff --git a/server/server.go b/server/server.go index dfc1da5..c674b23 100644 --- a/server/server.go +++ b/server/server.go @@ -206,7 +206,7 @@ func (s *Config) PostContext( if err != nil { return nil, fmt.Errorf("getting body: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() reply, err := io.ReadAll(resp.Body) if err != nil { @@ -235,7 +235,7 @@ func (s *Config) GetXMLContext(ctx context.Context, apiPath string, params url.V if err != nil { return err } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) @@ -244,7 +244,8 @@ func (s *Config) GetXMLContext(ctx context.Context, apiPath string, params url.V s.Username, s.URL+apiPath, resp.StatusCode, resp.Status, err, string(body)) } - if err = xml.NewDecoder(resp.Body).Decode(val); err != nil { + err = xml.NewDecoder(resp.Body).Decode(val) + if err != nil { return fmt.Errorf("reading body: %w", err) } @@ -269,7 +270,7 @@ func (s *Config) SimpleReqContext(ctx context.Context, apiURI string, params url if err != nil { return err } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode == http.StatusNotFound { return ErrNotFound @@ -321,7 +322,7 @@ func (s *Config) PostFormContext(ctx context.Context, apiPath string, form url.V if err != nil { return fmt.Errorf("http request: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() body, err := io.ReadAll(resp.Body) if err != nil { diff --git a/server/server_test.go b/server/server_test.go index e2dbe78..4fd8c66 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -51,7 +51,7 @@ func TestGet(t *testing.T) { require.NoError(t, err, "the method must not return an error when given a valid server to query") if err == nil { - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() check.Equal(http.StatusOK, resp.StatusCode, "the server must return a 200 response code") body, err := io.ReadAll(resp.Body) diff --git a/settings.go b/settings.go index 5d25b58..3bd1b46 100644 --- a/settings.go +++ b/settings.go @@ -13,7 +13,9 @@ var ErrCameraNumRequired = errors.New("cameraNum required") // GetGeneralSettings fetches ++settings-general. func (s *Server) GetGeneralSettings() (*GeneralSettings, error) { var val GeneralSettings - if err := s.GetXML("++settings-general", nil, &val); err != nil { + + err := s.GetXML("++settings-general", nil, &val) + if err != nil { return nil, fmt.Errorf("getting general settings: %w", err) } @@ -22,7 +24,8 @@ func (s *Server) GetGeneralSettings() (*GeneralSettings, error) { // SetGeneralSettings posts form fields to ++settings-general (partial update). func (s *Server) SetGeneralSettings(form url.Values) error { - if err := s.PostForm("++settings-general", form); err != nil { + err := s.PostForm("++settings-general", form) + if err != nil { return fmt.Errorf("setting general settings: %w", err) } @@ -32,7 +35,9 @@ func (s *Server) SetGeneralSettings(form url.Values) error { // GetDisplaySettings fetches ++settings-display. func (s *Server) GetDisplaySettings() (*DisplaySettings, error) { var val DisplaySettings - if err := s.GetXML("++settings-display", nil, &val); err != nil { + + err := s.GetXML("++settings-display", nil, &val) + if err != nil { return nil, fmt.Errorf("getting display settings: %w", err) } @@ -41,7 +46,8 @@ func (s *Server) GetDisplaySettings() (*DisplaySettings, error) { // SetDisplaySettings posts form fields to ++settings-display (partial update). func (s *Server) SetDisplaySettings(form url.Values) error { - if err := s.PostForm("++settings-display", form); err != nil { + err := s.PostForm("++settings-display", form) + if err != nil { return fmt.Errorf("setting display settings: %w", err) } @@ -51,7 +57,9 @@ func (s *Server) SetDisplaySettings(form url.Values) error { // GetStorageSettings fetches ++settings-storage. func (s *Server) GetStorageSettings() (*StorageSettings, error) { var val StorageSettings - if err := s.GetXML("++settings-storage", nil, &val); err != nil { + + err := s.GetXML("++settings-storage", nil, &val) + if err != nil { return nil, fmt.Errorf("getting storage settings: %w", err) } @@ -60,7 +68,8 @@ func (s *Server) GetStorageSettings() (*StorageSettings, error) { // SetStorageSettings posts form fields to ++settings-storage (partial update). func (s *Server) SetStorageSettings(form url.Values) error { - if err := s.PostForm("++settings-storage", form); err != nil { + err := s.PostForm("++settings-storage", form) + if err != nil { return fmt.Errorf("setting storage settings: %w", err) } @@ -70,7 +79,9 @@ func (s *Server) SetStorageSettings(form url.Values) error { // GetCompressionSettings fetches ++settings-compression. func (s *Server) GetCompressionSettings() (*CompressionSettings, error) { var val CompressionSettings - if err := s.GetXML("++settings-compression", nil, &val); err != nil { + + err := s.GetXML("++settings-compression", nil, &val) + if err != nil { return nil, fmt.Errorf("getting compression settings: %w", err) } @@ -79,7 +90,8 @@ func (s *Server) GetCompressionSettings() (*CompressionSettings, error) { // SetCompressionSettings posts form fields to ++settings-compression (partial update). func (s *Server) SetCompressionSettings(form url.Values) error { - if err := s.PostForm("++settings-compression", form); err != nil { + err := s.PostForm("++settings-compression", form) + if err != nil { return fmt.Errorf("setting compression settings: %w", err) } @@ -89,7 +101,9 @@ func (s *Server) SetCompressionSettings(form url.Values) error { // GetEmailSettings fetches ++settings-email. func (s *Server) GetEmailSettings() (*EmailSettings, error) { var val EmailSettings - if err := s.GetXML("++settings-email", nil, &val); err != nil { + + err := s.GetXML("++settings-email", nil, &val) + if err != nil { return nil, fmt.Errorf("getting email settings: %w", err) } @@ -98,7 +112,8 @@ func (s *Server) GetEmailSettings() (*EmailSettings, error) { // SetEmailSettings posts form fields to ++settings-email (partial update). func (s *Server) SetEmailSettings(form url.Values) error { - if err := s.PostForm("++settings-email", form); err != nil { + err := s.PostForm("++settings-email", form) + if err != nil { return fmt.Errorf("setting email settings: %w", err) } @@ -108,7 +123,9 @@ func (s *Server) SetEmailSettings(form url.Values) error { // GetWebSettings fetches ++settings-web. func (s *Server) GetWebSettings() (*WebSettings, error) { var val WebSettings - if err := s.GetXML("++settings-web", nil, &val); err != nil { + + err := s.GetXML("++settings-web", nil, &val) + if err != nil { return nil, fmt.Errorf("getting web settings: %w", err) } @@ -117,7 +134,8 @@ func (s *Server) GetWebSettings() (*WebSettings, error) { // SetWebSettings posts form fields to ++settings-web (partial update). func (s *Server) SetWebSettings(form url.Values) error { - if err := s.PostForm("++settings-web", form); err != nil { + err := s.PostForm("++settings-web", form) + if err != nil { return fmt.Errorf("setting web settings: %w", err) } @@ -130,7 +148,9 @@ func (s *Server) GetCameraSettings(cameraNum int) (*CameraSettings, error) { params.Set("cameraNum", strconv.Itoa(cameraNum)) var val CameraSettings - if err := s.GetXML("++settings-cameras", params, &val); err != nil { + + err := s.GetXML("++settings-cameras", params, &val) + if err != nil { return nil, fmt.Errorf("getting camera settings: %w", err) } @@ -144,7 +164,8 @@ func (s *Server) SetCameraSettings(form url.Values) error { return ErrCameraNumRequired } - if err := s.PostForm("++settings-cameras", form); err != nil { + err := s.PostForm("++settings-cameras", form) + if err != nil { return fmt.Errorf("setting camera settings: %w", err) } From 7d1be56b553d64e217bf73e59eec54ef2379624d Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Wed, 23 Sep 2026 00:19:47 -0700 Subject: [PATCH 4/5] Restore plain Close defers and the nlreturn exclusion. errcheck ignores Close, and the early returns stay on the same two lines. Co-authored-by: Cursor --- .golangci.yml | 5 +++++ cameras.go | 2 +- files.go | 4 ++-- internal/rtspclip/clip.go | 2 +- securityspy_test.go | 3 --- server/server.go | 8 ++++---- server/server_test.go | 2 +- 7 files changed, 14 insertions(+), 12 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 2e30ef0..cc7c33e 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -7,7 +7,12 @@ linters: - exhaustruct_v5 # camera and XML structs are filled a field at a time. - gomodguard # deprecated. - depguard # not useful. + - nlreturn settings: + errcheck: + exclude-functions: + - (*os.File).Close + - (io.Closer).Close exhaustive: default-signifies-exhaustive: true gocritic: diff --git a/cameras.go b/cameras.go index 5275ae1..be7aef6 100644 --- a/cameras.go +++ b/cameras.go @@ -407,7 +407,7 @@ func (c *Camera) Modes() (*CameraModes, error) { if err != nil { return nil, fmt.Errorf("getting camera modes: %w", err) } - defer func() { _ = resp.Body.Close() }() + defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("%w: %s", ErrCameraModesStatus, resp.Status) diff --git a/files.go b/files.go index efde94f..4de511a 100644 --- a/files.go +++ b/files.go @@ -156,13 +156,13 @@ func (f *File) Save(path string) (int64, error) { if err != nil { return 0, err } - defer func() { _ = body.Close() }() + defer body.Close() newFile, err := os.Create(path) //nolint:gosec // we are creating a file in a safe way. if err != nil { return 0, fmt.Errorf("os.Create(): %w", err) } - defer func() { _ = newFile.Close() }() + defer newFile.Close() size, err := io.Copy(newFile, body) if err != nil { diff --git a/internal/rtspclip/clip.go b/internal/rtspclip/clip.go index 3d3749a..3c8d419 100644 --- a/internal/rtspclip/clip.go +++ b/internal/rtspclip/clip.go @@ -99,7 +99,7 @@ func SaveMP4(ctx context.Context, rtspURL, path string, opts Options) (*Result, return nil, fmt.Errorf("create output: %w", err) } - defer func() { _ = file.Close() }() + defer file.Close() res, err := capture(ctx, rtspURL, opts, file, false) if err != nil { diff --git a/securityspy_test.go b/securityspy_test.go index 42093f5..3c80c88 100644 --- a/securityspy_test.go +++ b/securityspy_test.go @@ -96,7 +96,6 @@ func TestGetSounds(t *testing.T) { //nolint:dupl // it just looks like a duplica fakeServer := httptest.NewServer(http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { if req.URL.Path != "/++sounds" { http.NotFound(resp, req) - return } @@ -133,7 +132,6 @@ func TestGetScripts(t *testing.T) { //nolint:dupl // it just looks like a duplic fakeServer := httptest.NewServer(http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { if req.URL.Path != "/++scripts" { http.NotFound(resp, req) - return } @@ -219,7 +217,6 @@ func TestRefreshHandlesNilPTZAndMissingSchedules(t *testing.T) { fakeServer := httptest.NewServer(http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { if req.URL.Path != "/++systemInfo" { http.NotFound(resp, req) - return } diff --git a/server/server.go b/server/server.go index c674b23..06d4119 100644 --- a/server/server.go +++ b/server/server.go @@ -206,7 +206,7 @@ func (s *Config) PostContext( if err != nil { return nil, fmt.Errorf("getting body: %w", err) } - defer func() { _ = resp.Body.Close() }() + defer resp.Body.Close() reply, err := io.ReadAll(resp.Body) if err != nil { @@ -235,7 +235,7 @@ func (s *Config) GetXMLContext(ctx context.Context, apiPath string, params url.V if err != nil { return err } - defer func() { _ = resp.Body.Close() }() + defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) @@ -270,7 +270,7 @@ func (s *Config) SimpleReqContext(ctx context.Context, apiURI string, params url if err != nil { return err } - defer func() { _ = resp.Body.Close() }() + defer resp.Body.Close() if resp.StatusCode == http.StatusNotFound { return ErrNotFound @@ -322,7 +322,7 @@ func (s *Config) PostFormContext(ctx context.Context, apiPath string, form url.V if err != nil { return fmt.Errorf("http request: %w", err) } - defer func() { _ = resp.Body.Close() }() + defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { diff --git a/server/server_test.go b/server/server_test.go index 4fd8c66..e2dbe78 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -51,7 +51,7 @@ func TestGet(t *testing.T) { require.NoError(t, err, "the method must not return an error when given a valid server to query") if err == nil { - defer func() { _ = resp.Body.Close() }() + defer resp.Body.Close() check.Equal(http.StatusOK, resp.StatusCode, "the server must return a 200 response code") body, err := io.ReadAll(resp.Body) From 3cce12b557ccc14c9b0c7de1e5c57e4d6dd96459 Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Wed, 23 Sep 2026 00:43:21 -0700 Subject: [PATCH 5/5] Drop the unreachable GetFile length check and fix leftover typos. Codespell 2.4.3 does not know these words, so the new job stays green while the comments and assertion text stay wrong. Co-authored-by: Cursor --- files.go | 7 ++----- ptz.go | 2 +- securityspy_test.go | 6 +++--- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/files.go b/files.go index 4de511a..baec330 100644 --- a/files.go +++ b/files.go @@ -17,8 +17,8 @@ import ( const ( // DownloadDateFormat is the format the SecuritySpy ++download method accepts. // This matches the ++download inputs AND the folder names files are saved into. - // The file1/file2 inputs this gets passed into are actually undocuemnted and were - // created specifically for programmtic SDK access (ie. this library). + // The file1/file2 inputs this gets passed into are actually undocumented and were + // created specifically for programmatic SDK access (ie. this library). DownloadDateFormat = "2006-01-02" // FileDateFormat is an arbitrary date format used for saved files; we hope doesn't change. // This is used in the actual name of files that are saved. No where else. @@ -116,9 +116,6 @@ func (f *Files) GetFile(name string) (*File, error) { } nameDateSplit := strings.Split(fileExtSplit[0], " ") - if len(fileExtSplit) < fileParts { - return file, ErrInvalidName - } file.Updated, err = time.Parse(FileDateFormat, nameDateSplit[0]) if err != nil { diff --git a/ptz.go b/ptz.go index fad8ea9..d6326c3 100644 --- a/ptz.go +++ b/ptz.go @@ -198,7 +198,7 @@ func (z *PTZ) Stop() error { return z.ptzReq(ptzCommandStop) } -// UnmarshalXML method converts ptzCapbilities bitmask from an XML payload into true/false abilities. +// UnmarshalXML method converts ptzCapabilities bitmask from an XML payload into true/false abilities. // This isn't a method you should ever call directly; it is only used during data initialization. func (z *PTZ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { err := d.DecodeElement(&z.rawCaps, &start) diff --git a/securityspy_test.go b/securityspy_test.go index 3c80c88..f5f7f39 100644 --- a/securityspy_test.go +++ b/securityspy_test.go @@ -30,7 +30,7 @@ func TestGetServer(t *testing.T) { secspyServer, err := securityspy.New(&server.Config{Username: user, Password: pass, URL: URL, VerifySSL: true}) require.Error(t, err, "there is no server at the address provided so an error must exist") - check.NotNil(secspyServer, "server must not be nil. even wiuth an error it must be returned") + check.NotNil(secspyServer, "server must not be nil. even with an error it must be returned") if !strings.Contains(err.Error(), "target machine actively refused it") && !strings.Contains(err.Error(), "connection refused") { @@ -118,7 +118,7 @@ func TestGetSounds(t *testing.T) { //nolint:dupl // it just looks like a duplica sounds, err := secspyServer.GetSounds() require.NoError(t, err, "the method must not return an error when given valid XML to unmarshal") check.Len(sounds, 20, "all 20 sounds must exist in the slice") - check.Equal("Beeps.aif", sounds[0], "the sound files were not properly unmarhsalled") + check.Equal("Beeps.aif", sounds[0], "the sound files were not properly unmarshalled") _, err = secspyServer.GetSounds() require.Error(t, err) @@ -154,7 +154,7 @@ func TestGetScripts(t *testing.T) { //nolint:dupl // it just looks like a duplic scripts, err := secspyServer.GetScripts() require.NoError(t, err, "the method must not return an error when given valid XML to unmarshal") check.Len(scripts, 16, "all 16 scripts must exist in the slice") - check.Equal("Web-i Activate Relay 1.scpt", scripts[0], "the script files were not properly unmarhsalled") + check.Equal("Web-i Activate Relay 1.scpt", scripts[0], "the script files were not properly unmarshalled") _, err = secspyServer.GetScripts() require.Error(t, err)