diff --git a/.github/README.md b/.github/README.md
index 5a641a5..9084b46 100644
--- a/.github/README.md
+++ b/.github/README.md
@@ -1,3 +1,3 @@
-# Spotitube
+# Spotitube
Documentation available at [davidepucci.it/doc/spotitube](https://davidepucci.it/doc/spotitube).
diff --git a/.github/linters/.hadolint.yaml b/.github/linters/.hadolint.yaml
new file mode 100644
index 0000000..2200bac
--- /dev/null
+++ b/.github/linters/.hadolint.yaml
@@ -0,0 +1,6 @@
+---
+failure-threshold: warning
+
+ignored:
+ - DL3018 # Pin versions in apk add
+ - SC2215 # This flag is used as a command name
diff --git a/.github/linters/.jscpd.json b/.github/linters/.jscpd.json
new file mode 100644
index 0000000..d015e48
--- /dev/null
+++ b/.github/linters/.jscpd.json
@@ -0,0 +1,6 @@
+{
+ "threshold": 3,
+ "reporters": ["consoleFull"],
+ "ignore": ["**/__snapshots__/**", "**/node_modules/**", "**/*_test.go"],
+ "absolute": true
+}
diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml
index 64d973d..a57c3c7 100644
--- a/.github/workflows/push.yml
+++ b/.github/workflows/push.yml
@@ -8,7 +8,21 @@ on:
branches:
- master
-permissions: write-all
+permissions:
+ actions: read
+ attestations: none
+ checks: none
+ contents: read
+ deployments: none
+ id-token: none
+ issues: none
+ discussions: none
+ packages: write
+ pages: none
+ pull-requests: read
+ repository-projects: none
+ security-events: none
+ statuses: none
jobs:
commitlint:
@@ -25,18 +39,23 @@ jobs:
- uses: codespell-project/actions-codespell@v2
with:
check_filenames: true
- golangci-lint:
+ super-linter:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- - uses: golangci/golangci-lint-action@v6
+ - uses: super-linter/super-linter@latest
+ env:
+ GITHUB_TOKEN: ${{ secrets.GH_ACTIONS_SPOTITUBE }}
+ VALIDATE_ALL_CODEBASE: false
+ VALIDATE_GO: false
gofumpt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- run: |
- export PATH=$PATH:$(go env GOPATH)/bin
+ PATH="$PATH:$(go env GOPATH)/bin"
+ export PATH
go install mvdan.cc/gofumpt@latest
if gofumpt -l -e . | grep '^' -q; then exit 1; fi
go-channel-closure:
@@ -56,7 +75,16 @@ jobs:
- run: test "$(grep -cr 'defer gomonkey\.')" = "$(grep -cr 'Reset()$')"
test:
runs-on: ubuntu-latest
- needs: [commitlint, codespell, golangci-lint, gofumpt, go-channel-closure, go-http-body-closure, go-monkey-unpatch]
+ needs:
+ [
+ commitlint,
+ codespell,
+ super-linter,
+ gofumpt,
+ go-channel-closure,
+ go-http-body-closure,
+ go-monkey-unpatch,
+ ]
if: success()
steps:
- uses: actions/checkout@v4
diff --git a/.github/workflows/scrape.yml b/.github/workflows/scrape.yml
index a1ebcc8..174286c 100644
--- a/.github/workflows/scrape.yml
+++ b/.github/workflows/scrape.yml
@@ -12,7 +12,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- - run: go test -v -run ^TestScraping$ ./...
+ - run: go test -v -run ^TestScraping$ ./...
env:
TEST_SCRAPING: true
GENIUS_TOKEN: ${{ secrets.GENIUS_TOKEN }}
diff --git a/.github/workflows/tag.yml b/.github/workflows/tag.yml
index 465fbc2..c3ae169 100644
--- a/.github/workflows/tag.yml
+++ b/.github/workflows/tag.yml
@@ -3,9 +3,23 @@ name: tag
on:
push:
tags:
- - 'v*.*.*'
+ - "v*.*.*"
-permissions: write-all
+permissions:
+ actions: read
+ attestations: none
+ checks: none
+ contents: read
+ deployments: none
+ id-token: none
+ issues: none
+ discussions: none
+ packages: write
+ pages: none
+ pull-requests: read
+ repository-projects: none
+ security-events: none
+ statuses: none
jobs:
test:
diff --git a/Dockerfile b/Dockerfile
index 012a343..4bca2d4 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -2,20 +2,23 @@
FROM golang:alpine AS builder
WORKDIR /workspace
COPY . .
-RUN go mod download
-RUN --mount=type=secret,id=SPOTIFY_ID \
+RUN go mod download && \
+ --mount=type=secret,id=SPOTIFY_ID \
--mount=type=secret,id=SPOTIFY_KEY \
--mount=type=secret,id=GENIUS_TOKEN \
go build -ldflags="-s -w -X github.com/streambinder/spotitube/spotify.fallbackSpotifyID=$(cat /run/secrets/SPOTIFY_ID) -X github.com/streambinder/spotitube/spotify.fallbackSpotifyKey=$(cat /run/secrets/SPOTIFY_KEY) -X github.com/streambinder/spotitube/lyrics.fallbackGeniusToken=$(cat /run/secrets/GENIUS_TOKEN)"
-FROM alpine:latest
-RUN apk add --no-cache ffmpeg yt-dlp
-RUN mkdir /data
-RUN mkdir /cache
+FROM alpine:3
+RUN apk add --no-cache ffmpeg yt-dlp && \
+ mkdir /data && \
+ mkdir /cache && \
+ adduser -S spotitube
+USER spotitube
WORKDIR /data
ENV XDG_MUSIC_DIR=/data
ENV XDG_CACHE_HOME=/cache
COPY --from=builder /workspace/spotitube /usr/sbin/
+HEALTHCHECK CMD [ "/usr/sbin/spotitube", "--help" ]
EXPOSE 65535/tcp
ENTRYPOINT ["/usr/sbin/spotitube"]
LABEL org.opencontainers.image.source=https://github.com/streambinder/spotitube
diff --git a/cmd/attach.go b/cmd/attach.go
index f8d5b8e..208d274 100644
--- a/cmd/attach.go
+++ b/cmd/attach.go
@@ -26,9 +26,9 @@ func cmdAttach() *cobra.Command {
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
var (
- path = args[0]
- id = args[1]
- rename, _ = cmd.Flags().GetBool("rename")
+ path = args[0]
+ id = args[1]
+ rename = util.ErrWrap(false)(cmd.Flags().GetBool("rename"))
)
localTrack, err := id3.Open(path, id3v2.Options{Parse: false})
diff --git a/cmd/auth.go b/cmd/auth.go
index 3a039ac..21383f4 100644
--- a/cmd/auth.go
+++ b/cmd/auth.go
@@ -25,10 +25,10 @@ func cmdAuth() *cobra.Command {
Use: "auth",
Short: "Establish a Spotify session for future uses",
SilenceUsage: true,
- RunE: func(cmd *cobra.Command, args []string) error {
+ RunE: func(cmd *cobra.Command, _ []string) error {
var (
- remote, _ = cmd.Flags().GetBool("remote")
- logout, _ = cmd.Flags().GetBool("logout")
+ remote = util.ErrWrap(false)(cmd.Flags().GetBool("remote"))
+ logout = util.ErrWrap(false)(cmd.Flags().GetBool("logout"))
callback = "127.0.0.1"
processor = spotify.BrowserProcessor
)
diff --git a/cmd/auth_test.go b/cmd/auth_test.go
index 60fa21c..dac68fd 100644
--- a/cmd/auth_test.go
+++ b/cmd/auth_test.go
@@ -24,7 +24,7 @@ func TestCmdAuth(t *testing.T) {
return nil
}).
ApplyFunc(spotify.Authenticate, func() (*spotify.Client, error) {
- _ = printProcessor("")
+ util.ErrSuppress(printProcessor(""))
return &spotify.Client{}, nil
}).
Reset()
diff --git a/cmd/lookup.go b/cmd/lookup.go
index ef0944f..20f4e46 100644
--- a/cmd/lookup.go
+++ b/cmd/lookup.go
@@ -30,17 +30,18 @@ func cmdLookup() *cobra.Command {
Short: "Utility to lookup for tracks in order to investigate general querying behaviour",
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
- library, _ := cmd.Flags().GetBool("library")
- random, _ := cmd.Flags().GetBool("random")
- randomSize, _ := cmd.Flags().GetInt("random-size")
- libraryLimit, _ := cmd.Flags().GetInt("library-limit")
+ library := util.ErrWrap(false)(cmd.Flags().GetBool("library"))
+ random := util.ErrWrap(false)(cmd.Flags().GetBool("random"))
+ randomSize := util.ErrWrap(defaultRandomSize)(cmd.Flags().GetInt("random-size"))
+ libraryLimit := util.ErrWrap(0)(cmd.Flags().GetInt("library-limit"))
if !library && !random && len(args) == 0 {
return errors.New("no track has been issued")
}
- client, err := spotify.Authenticate(spotify.BrowserProcessor)
- if err != nil {
- return err
+ var authErr error
+ spotifyClient, authErr = spotify.Authenticate(spotify.BrowserProcessor)
+ if authErr != nil {
+ return authErr
}
var (
@@ -48,56 +49,9 @@ func cmdLookup() *cobra.Command {
lyricsChannel = make(chan interface{}, 1)
)
return nursery.RunConcurrently(
- func(ctx context.Context, ch chan error) {
- defer close(providerChannel)
- defer close(lyricsChannel)
- if random {
- if err := client.Random(spotify.TypeTrack, randomSize, providerChannel, lyricsChannel); err != nil {
- ch <- err
- return
- }
- } else if library {
- if err := client.Library(libraryLimit, providerChannel, lyricsChannel); err != nil {
- ch <- err
- return
- }
- } else {
- for _, id := range args {
- if _, err := client.Track(id, providerChannel, lyricsChannel); err != nil {
- ch <- err
- return
- }
- }
- }
- },
- func(ctx context.Context, ch chan error) {
- prefix := "[P]"
- for event := range providerChannel {
- track := event.(*entity.Track)
- matches, err := provider.Search(track)
- if err != nil {
- fmt.Println(colorRed+prefix, track.ID, util.Pad(track.Artists[0]), util.Pad(track.Title), err, colorReset)
- } else if len(matches) == 0 {
- fmt.Println(colorRed+prefix, track.ID, util.Pad(track.Artists[0]), util.Pad(track.Title), "no result", colorReset)
- } else {
- fmt.Println(prefix, track.ID, util.Pad(track.Artists[0]), util.Pad(track.Title), matches[0].URL, matches[0].Score)
- }
- }
- },
- func(ctx context.Context, ch chan error) {
- prefix := "[L]"
- for event := range lyricsChannel {
- track := event.(*entity.Track)
- lyrics, err := lyrics.Search(track)
- if err != nil {
- fmt.Println(colorRed+prefix, track.ID, util.Pad(track.Artists[0]), util.Pad(track.Title), err, colorReset)
- } else if len(lyrics) == 0 {
- fmt.Println(colorRed+prefix, track.ID, util.Pad(track.Artists[0]), util.Pad(track.Title), "no result", colorReset)
- } else {
- fmt.Println(prefix, track.ID, util.Pad(track.Artists[0]), util.Pad(track.Title), util.Excerpt(lyrics, 80))
- }
- }
- },
+ routineLookupFetch(random, library, randomSize, libraryLimit, args, providerChannel, lyricsChannel),
+ routineLookupProvider(providerChannel),
+ routineLookupLyrics(lyricsChannel),
)
},
}
@@ -107,3 +61,66 @@ func cmdLookup() *cobra.Command {
cmd.Flags().Int("library-limit", 0, "Number of tracks to fetch from library (unlimited if 0)")
return cmd
}
+
+func routineLookupFetch(random, library bool, randomSize, libraryLimit int, ids []string, providerChannel, lyricsChannel chan interface{}) func(context.Context, chan error) {
+ return func(_ context.Context, ch chan error) {
+ defer close(providerChannel)
+ defer close(lyricsChannel)
+
+ switch {
+ case random:
+ if err := spotifyClient.Random(spotify.TypeTrack, randomSize, providerChannel, lyricsChannel); err != nil {
+ ch <- err
+ return
+ }
+ case library:
+ if err := spotifyClient.Library(libraryLimit, providerChannel, lyricsChannel); err != nil {
+ ch <- err
+ return
+ }
+ default:
+ for _, id := range ids {
+ if _, err := spotifyClient.Track(id, providerChannel, lyricsChannel); err != nil {
+ ch <- err
+ return
+ }
+ }
+ }
+ }
+}
+
+func routineLookupProvider(providerChannel chan interface{}) func(context.Context, chan error) {
+ return func(context.Context, chan error) {
+ prefix := "[P]"
+ for event := range providerChannel {
+ track := event.(*entity.Track)
+ matches, err := provider.Search(track)
+ switch {
+ case err != nil:
+ fmt.Println(colorRed+prefix, track.ID, util.Pad(track.Artists[0]), util.Pad(track.Title), err, colorReset)
+ case len(matches) == 0:
+ fmt.Println(colorRed+prefix, track.ID, util.Pad(track.Artists[0]), util.Pad(track.Title), "no result", colorReset)
+ default:
+ fmt.Println(prefix, track.ID, util.Pad(track.Artists[0]), util.Pad(track.Title), matches[0].URL, matches[0].Score)
+ }
+ }
+ }
+}
+
+func routineLookupLyrics(lyricsChannel chan interface{}) func(context.Context, chan error) {
+ return func(context.Context, chan error) {
+ prefix := "[L]"
+ for event := range lyricsChannel {
+ track := event.(*entity.Track)
+ lyrics, err := lyrics.Search(track)
+ switch {
+ case err != nil:
+ fmt.Println(colorRed+prefix, track.ID, util.Pad(track.Artists[0]), util.Pad(track.Title), err, colorReset)
+ case len(lyrics) == 0:
+ fmt.Println(colorRed+prefix, track.ID, util.Pad(track.Artists[0]), util.Pad(track.Title), "no result", colorReset)
+ default:
+ fmt.Println(prefix, track.ID, util.Pad(track.Artists[0]), util.Pad(track.Title), util.Excerpt(lyrics, 80))
+ }
+ }
+ }
+}
diff --git a/cmd/lookup_test.go b/cmd/lookup_test.go
index 701fdb5..58e2ffc 100644
--- a/cmd/lookup_test.go
+++ b/cmd/lookup_test.go
@@ -110,7 +110,7 @@ func TestCmdLookupLibraryFailure(t *testing.T) {
ApplyFunc(spotify.Authenticate, func() (*spotify.Client, error) {
return &spotify.Client{}, nil
}).
- ApplyMethod(&spotify.Client{}, "Library", func(_ *spotify.Client, _ int, ch ...chan interface{}) error {
+ ApplyMethod(&spotify.Client{}, "Library", func(_ *spotify.Client, _ int, _ ...chan interface{}) error {
return errors.New("ko")
}).
Reset()
@@ -125,7 +125,7 @@ func TestCmdLookupTrackFailure(t *testing.T) {
ApplyFunc(spotify.Authenticate, func() (*spotify.Client, error) {
return &spotify.Client{}, nil
}).
- ApplyMethod(&spotify.Client{}, "Track", func(_ *spotify.Client, _ string, ch ...chan interface{}) (*entity.Track, error) {
+ ApplyMethod(&spotify.Client{}, "Track", func(_ *spotify.Client, _ string, _ ...chan interface{}) (*entity.Track, error) {
return nil, errors.New("ko")
}).Reset()
diff --git a/cmd/reset.go b/cmd/reset.go
index e40b0e7..4fcd533 100644
--- a/cmd/reset.go
+++ b/cmd/reset.go
@@ -20,9 +20,9 @@ func cmdReset() *cobra.Command {
Short: "Clear cached objects",
SilenceUsage: true,
Args: cobra.NoArgs,
- RunE: func(cmd *cobra.Command, args []string) error {
+ RunE: func(cmd *cobra.Command, _ []string) error {
var (
- session, _ = cmd.Flags().GetBool("session")
+ session = util.ErrWrap(false)(cmd.Flags().GetBool("session"))
cacheDirectory = util.CacheDirectory()
)
return filepath.WalkDir(cacheDirectory, func(path string, entry fs.DirEntry, err error) error {
diff --git a/cmd/reset_test.go b/cmd/reset_test.go
index ea90390..95dff74 100644
--- a/cmd/reset_test.go
+++ b/cmd/reset_test.go
@@ -43,10 +43,10 @@ func BenchmarkReset(b *testing.B) {
func TestCmdReset(t *testing.T) {
// monkey patching
defer gomonkey.NewPatches().
- ApplyFunc(filepath.WalkDir, func(path string, f func(string, fs.DirEntry, error) error) error {
- _ = f("", DirEntry{name: "", isDir: false}, errors.New("some error"))
- _ = f(spotify.TokenBasename, DirEntry{name: spotify.TokenBasename, isDir: false}, nil)
- _ = f("fname.txt", DirEntry{name: "fname.txt", isDir: false}, nil)
+ ApplyFunc(filepath.WalkDir, func(_ string, f func(string, fs.DirEntry, error) error) error {
+ util.ErrSuppress(f("", DirEntry{name: "", isDir: false}, errors.New("some error")))
+ util.ErrSuppress(f(spotify.TokenBasename, DirEntry{name: spotify.TokenBasename, isDir: false}, nil))
+ util.ErrSuppress(f("fname.txt", DirEntry{name: "fname.txt", isDir: false}, nil))
return nil
}).
ApplyFunc(os.RemoveAll, func() error {
diff --git a/cmd/root.go b/cmd/root.go
index 702fcea..98f0613 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -2,13 +2,18 @@ package cmd
import (
"github.com/spf13/cobra"
+ "github.com/streambinder/spotitube/spotify"
+ "github.com/streambinder/spotitube/util"
)
-var cmdRoot = &cobra.Command{
- Use: "spotitube",
- Short: "Synchronize Spotify collections downloading from external providers",
-}
+var (
+ spotifyClient *spotify.Client
+ cmdRoot = &cobra.Command{
+ Use: "spotitube",
+ Short: "Synchronize Spotify collections downloading from external providers",
+ }
+)
func Execute() {
- _ = cmdRoot.Execute()
+ util.ErrSuppress(cmdRoot.Execute())
}
diff --git a/cmd/root_test.go b/cmd/root_test.go
index d306db2..5ad1bbd 100644
--- a/cmd/root_test.go
+++ b/cmd/root_test.go
@@ -6,11 +6,12 @@ import (
"testing"
"github.com/spf13/cobra"
+ "github.com/streambinder/spotitube/util"
)
func BenchmarkRoot(b *testing.B) {
for i := 0; i < b.N; i++ {
- _ = testExecute(cmdRoot)
+ util.ErrSuppress(testExecute(cmdRoot))
}
}
@@ -25,7 +26,7 @@ func testExecute(cmd *cobra.Command, args ...string) error {
return cmdTest.Execute()
}
-func TestExecute(t *testing.T) {
+func TestExecute(_ *testing.T) {
cmdRoot.SetOut(io.Discard)
cmdRoot.SetErr(io.Discard)
cmdRoot.SetOutput(io.Discard)
diff --git a/cmd/show.go b/cmd/show.go
index 6e8a518..c29478b 100644
--- a/cmd/show.go
+++ b/cmd/show.go
@@ -24,7 +24,7 @@ func cmdShow() *cobra.Command {
Short: "Show local tracks data",
SilenceUsage: true,
Args: cobra.MinimumNArgs(1),
- RunE: func(cmd *cobra.Command, args []string) error {
+ RunE: func(_ *cobra.Command, args []string) error {
bold := color.New(color.Bold)
for _, path := range args {
if err := func() error {
diff --git a/cmd/sync.go b/cmd/sync.go
index 7c8118d..cdf887d 100644
--- a/cmd/sync.go
+++ b/cmd/sync.go
@@ -37,11 +37,10 @@ const (
)
var (
- spotifyClient *spotify.Client
routineSemaphores map[int](chan bool)
routineQueues map[int](chan interface{})
indexData = index.New()
- tui = anchor.Window(anchor.Red)
+ tui = anchor.New(anchor.Red)
)
func init() {
@@ -54,18 +53,18 @@ func cmdSync() *cobra.Command {
Short: "Synchronize collections",
SilenceUsage: true,
Args: cobra.NoArgs,
- RunE: func(cmd *cobra.Command, args []string) error {
+ RunE: func(cmd *cobra.Command, _ []string) error {
var (
- path, _ = cmd.Flags().GetString("output")
- playlistEncoding, _ = cmd.Flags().GetString("playlist-encoding")
- manual, _ = cmd.Flags().GetBool("manual")
- library, _ = cmd.Flags().GetBool("library")
- playlists, _ = cmd.Flags().GetStringArray("playlist")
- playlistsTracks, _ = cmd.Flags().GetStringArray("playlist-tracks")
- albums, _ = cmd.Flags().GetStringArray("album")
- tracks, _ = cmd.Flags().GetStringArray("track")
- fixes, _ = cmd.Flags().GetStringArray("fix")
- libraryLimit, _ = cmd.Flags().GetInt("library-limit")
+ path = util.ErrWrap(xdg.UserDirs.Music)(cmd.Flags().GetString("output"))
+ playlistEncoding = util.ErrWrap("m3u")(cmd.Flags().GetString("playlist-encoding"))
+ manual = util.ErrWrap(false)(cmd.Flags().GetBool("manual"))
+ library = util.ErrWrap(false)(cmd.Flags().GetBool("library"))
+ playlists = util.ErrWrap([]string{})(cmd.Flags().GetStringArray("playlist"))
+ playlistsTracks = util.ErrWrap([]string{})(cmd.Flags().GetStringArray("playlist-tracks"))
+ albums = util.ErrWrap([]string{})(cmd.Flags().GetStringArray("album"))
+ tracks = util.ErrWrap([]string{})(cmd.Flags().GetStringArray("track"))
+ fixes = util.ErrWrap([]string{})(cmd.Flags().GetStringArray("fix"))
+ libraryLimit = util.ErrWrap(0)(cmd.Flags().GetInt("library-limit"))
)
for index, path := range fixes {
@@ -89,7 +88,7 @@ func cmdSync() *cobra.Command {
routineMix(playlistEncoding),
)
},
- PreRun: func(cmd *cobra.Command, args []string) {
+ PreRun: func(cmd *cobra.Command, _ []string) {
routineSemaphores = map[int](chan bool){
routineTypeIndex: make(chan bool, 1),
routineTypeAuth: make(chan bool, 1),
@@ -104,16 +103,16 @@ func cmdSync() *cobra.Command {
}
var (
- playlists, _ = cmd.Flags().GetStringArray("playlist")
- playlistsTracks, _ = cmd.Flags().GetStringArray("playlist-tracks")
- albums, _ = cmd.Flags().GetStringArray("album")
- tracks, _ = cmd.Flags().GetStringArray("track")
- fixes, _ = cmd.Flags().GetStringArray("fix")
+ playlists = util.ErrWrap([]string{})(cmd.Flags().GetStringArray("playlist"))
+ playlistsTracks = util.ErrWrap([]string{})(cmd.Flags().GetStringArray("playlist-tracks"))
+ albums = util.ErrWrap([]string{})(cmd.Flags().GetStringArray("album"))
+ tracks = util.ErrWrap([]string{})(cmd.Flags().GetStringArray("track"))
+ fixes = util.ErrWrap([]string{})(cmd.Flags().GetStringArray("fix"))
)
if len(playlists)+len(playlistsTracks)+len(albums)+len(tracks)+len(fixes) == 0 {
cmd.LocalFlags().VisitAll(func(f *pflag.Flag) {
if f.Name == "library" {
- _ = f.Value.Set("true")
+ util.ErrSuppress(f.Value.Set("true"))
}
})
}
@@ -134,7 +133,7 @@ func cmdSync() *cobra.Command {
// indexer scans a possible local music library
// to be considered as already synchronized
-func routineIndex(ctx context.Context, ch chan error) {
+func routineIndex(_ context.Context, ch chan error) {
// remember to signal fetcher
defer close(routineSemaphores[routineTypeIndex])
@@ -151,7 +150,7 @@ func routineIndex(ctx context.Context, ch chan error) {
routineSemaphores[routineTypeIndex] <- true
}
-func routineAuth(ctx context.Context, ch chan error) {
+func routineAuth(_ context.Context, ch chan error) {
// remember to close auth semaphore
defer close(routineSemaphores[routineTypeAuth])
@@ -173,7 +172,7 @@ func routineAuth(ctx context.Context, ch chan error) {
// fetcher pulls data from the upstream
// provider, i.e. Spotify
func routineFetch(library bool, playlists, playlistsTracks, albums, tracks, fixes []string, libraryLimit int) func(ctx context.Context, ch chan error) {
- return func(ctx context.Context, ch chan error) {
+ return func(_ context.Context, ch chan error) {
// remember to stop passing data to decider and mixer
defer close(routineQueues[routineTypeDecide])
defer close(routineQueues[routineTypeMix])
@@ -197,67 +196,102 @@ func routineFetch(library bool, playlists, playlistsTracks, albums, tracks, fixe
tui.Lot("fetch").Close(fmt.Sprintf("%d tracks", counter))
}()
- if library {
- tui.Lot("fetch").Printf("library")
- if err := spotifyClient.Library(libraryLimit, routineQueues[routineTypeDecide], fetched); err != nil {
- ch <- err
- return
- }
+ fixesTracks, fixesErr := routineFetchFixesIDs(fixes)
+ if fixesErr != nil {
+ ch <- fixesErr
+ return
}
- for _, id := range albums {
- tui.Lot("fetch").Printf("album %s", id)
- if _, err := spotifyClient.Album(id, routineQueues[routineTypeDecide], fetched); err != nil {
- ch <- err
- return
- }
+ tracks = append(tracks, fixesTracks...)
+
+ if err := routineFetchLibrary(library, libraryLimit, fetched); err != nil {
+ ch <- err
+ return
}
- for _, path := range fixes {
- tui.Lot("fetch").Printf("track %s", path)
- tag, err := id3.Open(path, id3v2.Options{Parse: true})
- if err != nil {
- ch <- err
- return
- }
- id := tag.SpotifyID()
- if len(id) == 0 {
- ch <- errors.New("track " + path + " does not have spotify ID metadata set")
- return
- }
- tracks = append(tracks, id)
- indexData.SetPath(path, index.Flush)
+ if err := routineFetchAlbums(albums, fetched); err != nil {
+ ch <- err
+ return
+ }
+ if err := routineFetchTracks(tracks, fetched); err != nil {
+ ch <- err
+ return
+ }
+ if err := routineFetchPlaylists(append(playlists, playlistsTracks...), fetched); err != nil {
+ ch <- err
+ return
+ }
+ }
+}
- if err := tag.Close(); err != nil {
- ch <- err
- return
- }
+func routineFetchFixesIDs(fixes []string) ([]string, error) {
+ var localTracks []string
+ for _, path := range fixes {
+ tui.Lot("fetch").Printf("track %s", path)
+ tag, err := id3.Open(path, id3v2.Options{Parse: true})
+ if err != nil {
+ return nil, err
}
- for _, id := range tracks {
- tui.Lot("fetch").Printf("track %s", id)
- if _, err := spotifyClient.Track(id, routineQueues[routineTypeDecide], fetched); err != nil {
- ch <- err
- return
- }
+
+ id := tag.SpotifyID()
+ if len(id) == 0 {
+ return nil, errors.New("track " + path + " does not have spotify ID metadata set")
}
- // some special treatment for playlists
- for index, id := range append(playlists, playlistsTracks...) {
- tui.Lot("fetch").Printf("playlist %s", id)
- playlist, err := spotifyClient.Playlist(id, routineQueues[routineTypeDecide], fetched)
- if err != nil {
- ch <- err
- return
- }
- if index < len(playlists) {
- routineQueues[routineTypeMix] <- playlist
- }
+ localTracks = append(localTracks, id)
+ indexData.SetPath(path, index.Flush)
+ if err := tag.Close(); err != nil {
+ return nil, err
+ }
+ }
+ return localTracks, nil
+}
+
+func routineFetchLibrary(library bool, libraryLimit int, fetched chan interface{}) error {
+ if !library {
+ return nil
+ }
+
+ tui.Lot("fetch").Printf("library")
+ return spotifyClient.Library(libraryLimit, routineQueues[routineTypeDecide], fetched)
+}
+
+func routineFetchAlbums(albums []string, fetched chan interface{}) error {
+ for _, id := range albums {
+ tui.Lot("fetch").Printf("album %s", id)
+ if _, err := spotifyClient.Album(id, routineQueues[routineTypeDecide], fetched); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func routineFetchTracks(tracks []string, fetched chan interface{}) error {
+ for _, id := range tracks {
+ tui.Lot("fetch").Printf("track %s", id)
+ if _, err := spotifyClient.Track(id, routineQueues[routineTypeDecide], fetched); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func routineFetchPlaylists(playlists []string, fetched chan interface{}) error {
+ for index, id := range playlists {
+ tui.Lot("fetch").Printf("playlist %s", id)
+ playlist, err := spotifyClient.Playlist(id, routineQueues[routineTypeDecide], fetched)
+ if err != nil {
+ return err
+ }
+ if index < len(playlists) {
+ routineQueues[routineTypeMix] <- playlist
}
}
+ return nil
}
// decider finds the right asset to retrieve
// for a given track
func routineDecide(manualMode bool) func(context.Context, chan error) {
- return func(ctx context.Context, ch chan error) {
+ return func(_ context.Context, ch chan error) {
// remember to stop passing data to the collector
// the retriever, the composer and the painter
defer close(routineQueues[routineTypeCollect])
@@ -306,7 +340,7 @@ func routineDecide(manualMode bool) func(context.Context, chan error) {
// collector fetches all the needed assets
// for a blob to be processed (basically
// a wrapper around: retriever, composer and painter)
-func routineCollect(ctx context.Context, ch chan error) {
+func routineCollect(_ context.Context, ch chan error) {
// remember to stop passing data to installer
defer close(routineQueues[routineTypeProcess])
@@ -330,7 +364,7 @@ func routineCollect(ctx context.Context, ch chan error) {
// retriever pulls a track blob corresponding
// to the (meta)data fetched from upstream
func routineCollectAsset(track *entity.Track) func(context.Context, chan error) {
- return func(ctx context.Context, ch chan error) {
+ return func(_ context.Context, ch chan error) {
tui.Lot("download").Print(track.UpstreamURL)
if err := downloader.Download(track.UpstreamURL, track.Path().Download(), nil); err != nil {
tui.AnchorPrintf("download failure: %s", err)
@@ -345,7 +379,7 @@ func routineCollectAsset(track *entity.Track) func(context.Context, chan error)
// composer pulls lyrics to be inserted
// in the fetched blob
func routineCollectLyrics(track *entity.Track) func(context.Context, chan error) {
- return func(ctx context.Context, ch chan error) {
+ return func(_ context.Context, ch chan error) {
tui.Lot("compose").Printf("%s by %s", track.Title, track.Artists[0])
lyrics, err := lyrics.Search(track)
if err != nil {
@@ -362,7 +396,7 @@ func routineCollectLyrics(track *entity.Track) func(context.Context, chan error)
// painter pulls image blobs to be inserted
// as artworks in the fetched blob
func routineCollectArtwork(track *entity.Track) func(context.Context, chan error) {
- return func(ctx context.Context, ch chan error) {
+ return func(_ context.Context, ch chan error) {
artwork := make(chan []byte, 1)
defer close(artwork)
@@ -382,7 +416,7 @@ func routineCollectArtwork(track *entity.Track) func(context.Context, chan error
// postprocessor applies some further enhancements
// e.g. combining the downloaded artwork/lyrics
// into the blob
-func routineProcess(ctx context.Context, ch chan error) {
+func routineProcess(_ context.Context, ch chan error) {
// remember to stop passing data to installer
defer close(routineQueues[routineTypeInstall])
@@ -401,7 +435,7 @@ func routineProcess(ctx context.Context, ch chan error) {
}
// installer move the blob to its final destination
-func routineInstall(ctx context.Context, ch chan error) {
+func routineInstall(_ context.Context, ch chan error) {
// remember to signal mixer
defer close(routineSemaphores[routineTypeInstall])
@@ -424,7 +458,7 @@ func routineInstall(ctx context.Context, ch chan error) {
// mixer wraps playlists to their final destination
func routineMix(encoding string) func(context.Context, chan error) {
- return func(ctx context.Context, ch chan error) {
+ return func(_ context.Context, ch chan error) {
// block until installation is done
<-routineSemaphores[routineTypeInstall]
diff --git a/docs/README.md b/docs/README.md
index dd802bf..1bd8ed2 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1,3 +1,5 @@
+# Documentation
+
1. [About](about.md)
2. [Installation](installation.md)
3. [Design](design.md)
diff --git a/docs/about.md b/docs/about.md
index ea3c064..89bd6b7 100644
--- a/docs/about.md
+++ b/docs/about.md
@@ -1,6 +1,6 @@
# About
-![](assets/demo.gif)
+![demo](assets/demo.gif)
Spotitube is a CLI application to authenticate to Spotify account, fetch music collections — such as account library, playlists, albums or specific tracks —, look them up on a defined set of providers — such as YouTube —, download them and inflate the downloaded assets with metadata collected from Spotify, further enriched with lyrics.
@@ -55,8 +55,8 @@ docker run -it --rm \
The only real issue to be addressed when working with Spotitube running in headless mode, is the redirect during Spotify authentication.
-By default, once authenticated to Spotify via web, Spotify itself redirects to a predefined callback URL, which corresponds to http://localhost:65535.
-In order to make that redirect go against a custom server, on Spotitube Spotify app, a further callback URL has been defined, i.e. http://spotitube.local:65535.
+By default, once authenticated to Spotify via web, Spotify itself redirects to a predefined callback URL, which corresponds to `http://localhost:65535`.
+In order to make that redirect go against a custom server, on Spotitube Spotify app, a further callback URL has been defined, i.e. `http://spotitube.local:65535`.
This is the one that is set as callback at runtime when Spotitube goes through authentication with the `--remote` flag.
So, assuming the server on which Spotitube is running is reachable at 1.2.3.4, make sure the client can correctly resolve `spotitube.local` as 1.2.3.4.
diff --git a/docs/design.md b/docs/design.md
index 771e84b..d82575a 100644
--- a/docs/design.md
+++ b/docs/design.md
@@ -8,8 +8,7 @@ Such queues usually carry a specific track (be it part of synchronization of use
The assembly line is made of the following routines:
-
-![](assets/design.svg)
+![design](assets/design.svg)
## Indexer
diff --git a/downloader/blob.go b/downloader/blob.go
index 47427d3..c301617 100644
--- a/downloader/blob.go
+++ b/downloader/blob.go
@@ -19,7 +19,7 @@ func init() {
}
func (blob) supports(url string) bool {
- response, err := http.Head(url)
+ response, err := http.Head(url) // nolint
if err != nil {
return false
}
@@ -38,7 +38,7 @@ func (blob) supports(url string) bool {
}
func (blob) download(url, path string, processor processor.Processor, channels ...chan []byte) error {
- response, err := http.Get(url)
+ response, err := http.Get(url) // nolint
if err != nil {
return err
}
diff --git a/downloader/youtubedl.go b/downloader/youtubedl.go
index b386b10..47467f4 100644
--- a/downloader/youtubedl.go
+++ b/downloader/youtubedl.go
@@ -19,7 +19,7 @@ func (youTubeDl) supports(url string) bool {
return strings.Contains(url, "://youtu.be") || strings.Contains(url, "://youtube.com")
}
-func (youTubeDl) download(url, path string, processor processor.Processor, channels ...chan []byte) error {
+func (youTubeDl) download(url, path string, _ processor.Processor, channels ...chan []byte) error {
// in this case, data won't be passed through channels
// as too heavy
for _, ch := range channels {
diff --git a/entity/index/index.go b/entity/index/index.go
index d09854d..c78982f 100644
--- a/entity/index/index.go
+++ b/entity/index/index.go
@@ -102,7 +102,7 @@ func (index *Index) Size(statuses ...int) (counter int) {
for _, value := range index.data {
for _, status := range statuses {
if value == status {
- counter += 1
+ counter++
break
}
}
diff --git a/entity/index/index_test.go b/entity/index/index_test.go
index 61b9efd..fa7c35b 100644
--- a/entity/index/index_test.go
+++ b/entity/index/index_test.go
@@ -11,6 +11,7 @@ import (
"github.com/bogem/id3v2/v2"
"github.com/streambinder/spotitube/entity"
"github.com/streambinder/spotitube/entity/id3"
+ "github.com/streambinder/spotitube/util"
"github.com/stretchr/testify/assert"
)
@@ -46,10 +47,10 @@ func BenchmarkIndex(b *testing.B) {
func TestBuild(t *testing.T) {
// monkey patching
defer gomonkey.NewPatches().
- ApplyFunc(filepath.WalkDir, func(path string, f func(string, fs.DirEntry, error) error) error {
- _ = f("", nil, errors.New("ko"))
- _ = f("", DirEntry{name: "dir", isDir: true}, nil)
- _ = f("fname.txt", DirEntry{name: "", isDir: false}, nil)
+ ApplyFunc(filepath.WalkDir, func(_ string, f func(string, fs.DirEntry, error) error) error {
+ util.ErrSuppress(f("", nil, errors.New("ko")))
+ util.ErrSuppress(f("", DirEntry{name: "dir", isDir: true}, nil))
+ util.ErrSuppress(f("fname.txt", DirEntry{name: "", isDir: false}, nil))
return f("Artist - Title.mp3", DirEntry{name: "", isDir: false}, nil)
}).
ApplyFunc(id3.Open, func() (*id3.Tag, error) {
@@ -77,7 +78,7 @@ func TestBuild(t *testing.T) {
func TestBuildOpenFailure(t *testing.T) {
// monkey patching
defer gomonkey.NewPatches().
- ApplyFunc(filepath.WalkDir, func(path string, f func(string, fs.DirEntry, error) error) error {
+ ApplyFunc(filepath.WalkDir, func(_ string, f func(string, fs.DirEntry, error) error) error {
return f("fname.mp3", DirEntry{name: "", isDir: false}, nil)
}).
ApplyFunc(id3.Open, func() (*id3.Tag, error) {
diff --git a/entity/playlist/m3u.go b/entity/playlist/m3u.go
index b766409..a590fb5 100644
--- a/entity/playlist/m3u.go
+++ b/entity/playlist/m3u.go
@@ -38,5 +38,5 @@ func (encoder *M3UEncoder) Add(track *entity.Track) error {
}
func (encoder *M3UEncoder) Close() error {
- return os.WriteFile(encoder.target, encoder.data, 0o644)
+ return os.WriteFile(encoder.target, encoder.data, 0o600)
}
diff --git a/entity/playlist/pls.go b/entity/playlist/pls.go
index 6b12880..b38e43a 100644
--- a/entity/playlist/pls.go
+++ b/entity/playlist/pls.go
@@ -24,7 +24,7 @@ func (encoder *PLSEncoder) init(name string) error {
}
func (encoder *PLSEncoder) Add(track *entity.Track) error {
- encoder.entries += 1
+ encoder.entries++
encoder.data = append(encoder.data, []byte(
fmt.Sprintf("File%d=%s\nTitle%d=%s\nLength%d=%d\n\n",
encoder.entries,
@@ -42,5 +42,5 @@ func (encoder *PLSEncoder) Close() error {
encoder.data = append(encoder.data, []byte(
fmt.Sprintf("NumberOfEntries=%d\n", encoder.entries),
)...)
- return os.WriteFile(encoder.target, encoder.data, 0o644)
+ return os.WriteFile(encoder.target, encoder.data, 0o600)
}
diff --git a/entity/track.go b/entity/track.go
index 92d3d69..495e374 100644
--- a/entity/track.go
+++ b/entity/track.go
@@ -27,7 +27,7 @@ type Track struct {
UpstreamURL string // URL to the upstream blob the song's been downloaded from
}
-type trackPath struct {
+type TrackPath struct {
track *Track
}
@@ -53,27 +53,27 @@ func (track *Track) Song() (song string) {
return
}
-func (track *Track) Path() trackPath {
- return trackPath{track}
+func (track *Track) Path() TrackPath {
+ return TrackPath{track}
}
-func (trackPath trackPath) Final() string {
+func (trackPath TrackPath) Final() string {
return util.LegalizeFilename(fmt.Sprintf("%s - %s.%s", trackPath.track.Artists[0], trackPath.track.Title, TrackFormat))
}
-func (trackPath trackPath) Download() string {
+func (trackPath TrackPath) Download() string {
return util.CacheFile(
util.LegalizeFilename(fmt.Sprintf("%s.%s", slug.Make(trackPath.track.ID), TrackFormat)),
)
}
-func (trackPath trackPath) Artwork() string {
+func (trackPath TrackPath) Artwork() string {
return util.CacheFile(
util.LegalizeFilename(fmt.Sprintf("%s.%s", slug.Make(path.Base(trackPath.track.Artwork.URL)), ArtworkFormat)),
)
}
-func (trackPath trackPath) Lyrics() string {
+func (trackPath TrackPath) Lyrics() string {
return util.CacheFile(
util.LegalizeFilename(fmt.Sprintf("%s.%s", slug.Make(trackPath.track.ID), LyricsFormat)),
)
diff --git a/lyrics/composer.go b/lyrics/composer.go
index 892105d..f24c1c1 100644
--- a/lyrics/composer.go
+++ b/lyrics/composer.go
@@ -59,7 +59,7 @@ func Search(track *entity.Track) (string, error) {
return "", err
}
- return string(result), os.WriteFile(track.Path().Lyrics(), result, 0o644)
+ return string(result), os.WriteFile(track.Path().Lyrics(), result, 0o600)
}
func Get(url string) (string, error) {
diff --git a/lyrics/genius.go b/lyrics/genius.go
index 115cfbd..2db269c 100644
--- a/lyrics/genius.go
+++ b/lyrics/genius.go
@@ -90,7 +90,16 @@ func (composer genius) search(track *entity.Track, ctxs ...context.Context) ([]b
return nil, errors.New("cannot search lyrics on genius: " + response.Status)
}
- body, err := io.ReadAll(response.Body)
+ return composer.parseResult(track, query, mainArtistOnly, response.Body, ctxs...)
+}
+
+func (composer genius) parseResult(track *entity.Track, query string, mainArtistOnly bool, response io.Reader, ctxs ...context.Context) ([]byte, error) {
+ ctx := context.Background()
+ if len(ctxs) > 0 {
+ ctx = ctxs[0]
+ }
+
+ body, err := io.ReadAll(response)
if err != nil {
return nil, err
}
@@ -162,7 +171,7 @@ func (composer genius) get(url string, ctxs ...context.Context) ([]byte, error)
}
func documentParser(data *[]byte) func(i int, s *goquery.Selection) {
- return func(i int, s *goquery.Selection) {
+ return func(_ int, s *goquery.Selection) {
switch goquery.NodeName(s) {
case "br", "div":
*data = append(*data, 10)
diff --git a/lyrics/genius_test.go b/lyrics/genius_test.go
index b027183..b6c6673 100644
--- a/lyrics/genius_test.go
+++ b/lyrics/genius_test.go
@@ -123,7 +123,7 @@ func TestGeniusSearchHttpNotFound(t *testing.T) {
func TestGeniusSearchTooManyRequests(t *testing.T) {
// monkey patching
var (
- doApiCounter = 0
+ doAPICounter = 0
doCounter = 0
tooManyRequestsResponse = &http.Response{
StatusCode: 429,
@@ -135,8 +135,8 @@ func TestGeniusSearchTooManyRequests(t *testing.T) {
ApplyFunc(time.Sleep, func() {}).
ApplyPrivateMethod(reflect.TypeOf(http.DefaultClient), "do", func(_ *http.Client, request *http.Request) (*http.Response, error) {
if strings.EqualFold(request.Host, "api.genius.com") {
- doApiCounter++
- if doApiCounter > 1 {
+ doAPICounter++
+ if doAPICounter > 1 {
return &http.Response{
StatusCode: 200,
Body: io.NopCloser(
diff --git a/lyrics/lyricsovh.go b/lyrics/lyricsovh.go
index 946099e..e6b56fe 100644
--- a/lyrics/lyricsovh.go
+++ b/lyrics/lyricsovh.go
@@ -55,12 +55,13 @@ func (composer lyricsOvh) get(url string, ctxs ...context.Context) ([]byte, erro
}
defer response.Body.Close()
- if response.StatusCode == 404 {
+ switch {
+ case response.StatusCode == 404:
return nil, nil
- } else if response.StatusCode == 429 {
+ case response.StatusCode == 429:
util.SleepUntilRetry(response.Header)
return composer.get(url, ctx)
- } else if response.StatusCode != 200 {
+ case response.StatusCode != 200:
return nil, errors.New("cannot fetch results on lyrics.ovh: " + response.Status)
}
diff --git a/provider/provider.go b/provider/provider.go
index 7c7c829..aebf2bc 100644
--- a/provider/provider.go
+++ b/provider/provider.go
@@ -29,7 +29,7 @@ func Search(track *entity.Track) ([]*Match, error) {
)
for _, provider := range providers {
workers = append(workers, func(p Provider) func(ctx context.Context, ch chan error) {
- return func(ctx context.Context, ch chan error) {
+ return func(_ context.Context, ch chan error) {
scopedMatches, err := p.search(track)
if err != nil {
ch <- err
diff --git a/provider/youtube.go b/provider/youtube.go
index c340aaf..a5e2443 100644
--- a/provider/youtube.go
+++ b/provider/youtube.go
@@ -3,6 +3,7 @@ package provider
import (
"errors"
"fmt"
+ "io"
"math"
"net/http"
"net/url"
@@ -30,7 +31,7 @@ type youTubeInitialData struct {
ItemSectionRenderer struct {
Contents []struct {
VideoRenderer struct {
- VideoId string
+ VideoID string
Title struct {
Runs []Run
}
@@ -118,12 +119,15 @@ func (provider youTube) search(track *entity.Track) ([]*Match, error) {
return nil, errors.New("cannot fetch results on youtube: " + response.Status)
}
- document, err := goquery.NewDocumentFromReader(response.Body)
+ return provider.parseResults(track, query, response.Body)
+}
+
+func (provider youTube) parseResults(track *entity.Track, query string, body io.Reader) ([]*Match, error) {
+ document, err := goquery.NewDocumentFromReader(body)
if err != nil {
return nil, err
}
-
- json := strings.Join(document.Find("script").Map(func(i int, selection *goquery.Selection) string {
+ json := strings.Join(document.Find("script").Map(func(_ int, selection *goquery.Selection) string {
prefix := "var ytInitialData ="
if !strings.HasPrefix(strings.TrimPrefix(selection.Text(), " "), prefix) {
return ""
@@ -147,7 +151,7 @@ func (provider youTube) search(track *entity.Track) ([]*Match, error) {
match := youTubeResult{
track: track,
query: query,
- id: result.VideoRenderer.VideoId,
+ id: result.VideoRenderer.VideoID,
title: title.Text,
owner: result.VideoRenderer.OwnerText.Runs[run].Text,
description: util.First(result.VideoRenderer.DetailedMetadataSnippets, DetailedMetadataSnippet{
diff --git a/spotify/album.go b/spotify/album.go
index 222e20d..cd8b86b 100644
--- a/spotify/album.go
+++ b/spotify/album.go
@@ -33,7 +33,7 @@ func (client *Client) Album(target string, channels ...chan interface{}) (*entit
album := albumEntity(fullAlbum)
for {
for _, albumTrack := range fullAlbum.Tracks.Tracks {
- track := trackEntity(&spotify.FullTrack{
+ track := trackEntity(spotify.FullTrack{
SimpleTrack: albumTrack,
Album: fullAlbum.SimpleAlbum,
})
diff --git a/spotify/auth.go b/spotify/auth.go
index de47ab2..f59001e 100644
--- a/spotify/auth.go
+++ b/spotify/auth.go
@@ -8,6 +8,7 @@ import (
"net/http"
"os"
"path/filepath"
+ "time"
"github.com/arunsworld/nursery"
"github.com/streambinder/spotitube/util"
@@ -39,9 +40,13 @@ type Client struct {
func Authenticate(urlProcessor func(string) error, callbacks ...string) (*Client, error) {
var (
- client Client
- serverMux = http.NewServeMux()
- server = &http.Server{Addr: fmt.Sprintf("0.0.0.0:%d", port), Handler: serverMux}
+ client Client
+ serverMux = http.NewServeMux()
+ server = &http.Server{
+ Addr: fmt.Sprintf("0.0.0.0:%d", port),
+ Handler: serverMux,
+ ReadHeaderTimeout: 2 * time.Second,
+ }
state = randstr.Hex(20)
callback = "127.0.0.1"
clientChannel = make(chan *spotify.Client, 1)
@@ -92,7 +97,7 @@ func Authenticate(urlProcessor func(string) error, callbacks ...string) (*Client
if err := nursery.RunConcurrently(
// spawn web server to handle login redirection
- func(ctx context.Context, ch chan error) {
+ func(_ context.Context, ch chan error) {
if err := server.ListenAndServe(); err != http.ErrServerClosed {
ch <- err
clientChannel <- nil
@@ -100,7 +105,7 @@ func Authenticate(urlProcessor func(string) error, callbacks ...string) (*Client
}
},
// auto-launch web browser with authentication URL
- func(ctx context.Context, ch chan error) {
+ func(_ context.Context, ch chan error) {
if urlProcessor == nil {
return
}
diff --git a/spotify/auth_test.go b/spotify/auth_test.go
index 15328a9..feb8f76 100644
--- a/spotify/auth_test.go
+++ b/spotify/auth_test.go
@@ -88,10 +88,10 @@ func TestAuthenticate(t *testing.T) {
// testing
assert.Nil(t, nursery.RunConcurrently(
- func(ctx context.Context, ch chan error) {
+ func(_ context.Context, ch chan error) {
ch <- util.ErrOnly(Authenticate(BrowserProcessor, "127.0.0.1"))
},
- func(ctx context.Context, ch chan error) {
+ func(_ context.Context, _ chan error) {
var (
response *http.Response
err error
@@ -157,10 +157,10 @@ func TestAuthenticateRecoverOpenFailure(t *testing.T) {
// testing
assert.Nil(t, nursery.RunConcurrently(
- func(ctx context.Context, ch chan error) {
+ func(_ context.Context, ch chan error) {
ch <- util.ErrOnly(Authenticate(nil))
},
- func(ctx context.Context, ch chan error) {
+ func(_ context.Context, _ chan error) {
var (
response *http.Response
err error
@@ -205,10 +205,10 @@ func TestAuthenticateRecoverUnmarshalFailure(t *testing.T) {
// testing
assert.Nil(t, nursery.RunConcurrently(
- func(ctx context.Context, ch chan error) {
+ func(_ context.Context, ch chan error) {
ch <- util.ErrOnly(Authenticate(nil))
},
- func(ctx context.Context, ch chan error) {
+ func(_ context.Context, _ chan error) {
var (
response *http.Response
err error
@@ -304,10 +304,10 @@ func TestAuthenticateNotFound(t *testing.T) {
// testing
assert.EqualError(t, nursery.RunConcurrently(
- func(ctx context.Context, ch chan error) {
+ func(_ context.Context, ch chan error) {
ch <- util.ErrOnly(Authenticate(nil))
},
- func(ctx context.Context, ch chan error) {
+ func(_ context.Context, _ chan error) {
var (
response *http.Response
err error
@@ -349,10 +349,10 @@ func TestAuthenticateForbidden(t *testing.T) {
// testing
assert.EqualError(t, nursery.RunConcurrently(
- func(ctx context.Context, ch chan error) {
+ func(_ context.Context, ch chan error) {
ch <- util.ErrOnly(Authenticate(nil))
},
- func(ctx context.Context, ch chan error) {
+ func(_ context.Context, _ chan error) {
var (
response *http.Response
err error
@@ -391,12 +391,12 @@ func TestAuthenticateProcessorFailure(t *testing.T) {
// testing
assert.EqualError(t, nursery.RunConcurrently(
- func(ctx context.Context, ch chan error) {
+ func(_ context.Context, ch chan error) {
ch <- util.ErrOnly(Authenticate(func(_ string) error {
return errors.New("ko")
}))
},
- func(ctx context.Context, ch chan error) {
+ func(_ context.Context, _ chan error) {
var (
response *http.Response
err error
diff --git a/spotify/id_test.go b/spotify/id_test.go
index 2da5359..90d1f98 100644
--- a/spotify/id_test.go
+++ b/spotify/id_test.go
@@ -16,10 +16,10 @@ func BenchmarkID(b *testing.B) {
func TestID(t *testing.T) {
var (
target = "1234567890123456789012"
- spotifyId = spotify.ID(target)
+ spotifyID = spotify.ID(target)
)
- assert.Equal(t, id(target), spotifyId)
- assert.Equal(t, id("spotify:track:"+target), spotifyId)
- assert.Equal(t, id("https://open.spotify.com/track/"+target), spotifyId)
- assert.Equal(t, id("https://open.spotify.com/track/"+target+"?si=abcdefghijklmnop"), spotifyId)
+ assert.Equal(t, id(target), spotifyID)
+ assert.Equal(t, id("spotify:track:"+target), spotifyID)
+ assert.Equal(t, id("https://open.spotify.com/track/"+target), spotifyID)
+ assert.Equal(t, id("https://open.spotify.com/track/"+target+"?si=abcdefghijklmnop"), spotifyID)
}
diff --git a/spotify/library.go b/spotify/library.go
index 7d33ec9..e59d4a9 100644
--- a/spotify/library.go
+++ b/spotify/library.go
@@ -19,7 +19,7 @@ func (client *Client) Library(limit int, channels ...chan interface{}) error {
ctr := 0
for {
for _, libraryTrack := range library.Tracks {
- track := trackEntity(&libraryTrack.FullTrack)
+ track := trackEntity(libraryTrack.FullTrack)
for _, ch := range channels {
ch <- track
}
diff --git a/spotify/playlist.go b/spotify/playlist.go
index a386427..d43531d 100644
--- a/spotify/playlist.go
+++ b/spotify/playlist.go
@@ -13,7 +13,7 @@ const (
personalPlaylistsCacheID = "PersonalPlaylists"
)
-func playlistEntity(fullPlaylist *spotify.FullPlaylist) *playlist.Playlist {
+func playlistEntity(fullPlaylist spotify.FullPlaylist) *playlist.Playlist {
return &playlist.Playlist{
ID: fullPlaylist.ID.String(),
Name: fullPlaylist.Name,
@@ -54,7 +54,7 @@ func (client *Client) personalPlaylists() ([]*playlist.Playlist, error) {
for {
for _, playlist := range userPlaylists.Playlists {
- playlists = append(playlists, playlistEntity(&spotify.FullPlaylist{SimplePlaylist: playlist}))
+ playlists = append(playlists, playlistEntity(spotify.FullPlaylist{SimplePlaylist: playlist}))
}
if err := client.NextPage(ctx, userPlaylists); errors.Is(err, spotify.ErrNoMorePages) {
@@ -81,10 +81,10 @@ func (client *Client) Playlist(target string, channels ...chan interface{}) (*pl
return nil, err
}
- playlist := playlistEntity(fullPlaylist)
+ playlist := playlistEntity(*fullPlaylist)
for {
for _, playlistTrack := range fullPlaylist.Tracks.Tracks {
- track := trackEntity(&playlistTrack.Track)
+ track := trackEntity(playlistTrack.Track)
playlist.Tracks = append(playlist.Tracks, track)
for _, ch := range channels {
ch <- track
diff --git a/spotify/random.go b/spotify/random.go
index 6e265ce..004fcdf 100644
--- a/spotify/random.go
+++ b/spotify/random.go
@@ -20,7 +20,7 @@ func (client *Client) Random(searchType spotify.SearchType, amount int, channels
for {
for _, fullTrack := range search.Tracks.Tracks {
- track := trackEntity(&fullTrack)
+ track := trackEntity(fullTrack)
for _, ch := range channels {
ch <- track
}
diff --git a/spotify/track.go b/spotify/track.go
index 7d82d7f..f35387e 100644
--- a/spotify/track.go
+++ b/spotify/track.go
@@ -12,7 +12,7 @@ import (
const TypeTrack = spotify.SearchTypeTrack
-func trackEntity(track *spotify.FullTrack) *entity.Track {
+func trackEntity(track spotify.FullTrack) *entity.Track {
return &entity.Track{
ID: track.ID.String(),
Title: track.Name,
@@ -45,7 +45,7 @@ func (client *Client) Track(target string, channels ...chan interface{}) (*entit
if err != nil {
return nil, err
}
- track := trackEntity(fullTrack)
+ track := trackEntity(*fullTrack)
for _, ch := range channels {
ch <- track
diff --git a/util/anchor/lot.go b/util/anchor/lot.go
index 7b0ec20..c5bcec2 100644
--- a/util/anchor/lot.go
+++ b/util/anchor/lot.go
@@ -12,7 +12,7 @@ const idle = "idle"
var idleColor = color.New(color.FgWhite)
-type lot struct {
+type Lot struct {
anchor
id int
alias string
@@ -23,7 +23,7 @@ func formatAlias(alias string) string {
return fmt.Sprintf("(%s) ", alias)
}
-func (lot *lot) Print(message string) {
+func (lot *Lot) Print(message string) {
lot.window.lock.Lock()
defer lot.window.lock.Unlock()
defer cursor.Bottom()
@@ -36,20 +36,20 @@ func (lot *lot) Print(message string) {
}
}
-func (lot *lot) Printf(format string, a ...any) {
+func (lot *Lot) Printf(format string, a ...any) {
lot.Print(fmt.Sprintf(format, a...))
}
-func (lot *lot) Wipe() {
+func (lot *Lot) Wipe() {
lot.Print(idle)
}
-func (lot *lot) Close(messages ...string) {
+func (lot *Lot) Close(messages ...string) {
lot.style = color.New(color.FgWhite)
lot.Print(util.First(messages, "done"))
}
-func (lot *lot) write() {
+func (lot *Lot) write() {
dataStyle := lot.style
if lot.data == idle {
dataStyle = idleColor
diff --git a/util/anchor/window.go b/util/anchor/window.go
index 94a4f50..fc1c296 100644
--- a/util/anchor/window.go
+++ b/util/anchor/window.go
@@ -29,9 +29,9 @@ const (
type Color color.Attribute
-type window struct {
+type Window struct {
anchors []*anchor
- lots []*lot
+ lots []*Lot
aliases map[string]int
anchorColor *color.Color
lock sync.RWMutex
@@ -39,20 +39,20 @@ type window struct {
type anchor struct {
data string
- window *window
+ window *Window
}
-func Window(anchorColors ...color.Attribute) *window {
- return &window{
+func New(anchorColors ...color.Attribute) *Window {
+ return &Window{
anchors: []*anchor{},
- lots: []*lot{},
+ lots: []*Lot{},
aliases: make(map[string]int),
anchorColor: color.New(util.First(anchorColors, Normal)),
lock: sync.RWMutex{},
}
}
-func (window *window) Lot(alias string) *lot {
+func (window *Window) Lot(alias string) *Lot {
window.lock.Lock()
defer window.lock.Unlock()
@@ -60,7 +60,7 @@ func (window *window) Lot(alias string) *lot {
return window.lots[id]
}
- lot := &lot{
+ lot := &Lot{
anchor: anchor{
data: "",
window: window,
@@ -75,25 +75,25 @@ func (window *window) Lot(alias string) *lot {
return lot
}
-func (window *window) Printf(format string, a ...any) {
+func (window *Window) Printf(format string, a ...any) {
window.print(false, fmt.Sprintf(format, a...))
}
-func (window *window) AnchorPrintf(format string, a ...any) {
+func (window *Window) AnchorPrintf(format string, a ...any) {
window.print(true, window.anchorColor.Sprintf(format, a...))
}
-func (window *window) up(lines ...int) {
+func (window *Window) up(lines ...int) {
cursor.UpAndClear(util.First(lines, 1))
cursor.StartOfLine()
}
-func (window *window) down() {
+func (window *Window) down() {
cursor.DownAndClear(1)
cursor.StartOfLine()
}
-func (window *window) shift(lines int) {
+func (window *Window) shift(lines int) {
if lines <= 0 && lines != cursorAnchor && lines != cursorDefault {
return
}
@@ -118,7 +118,7 @@ func (window *window) shift(lines int) {
}
}
-func (window *window) print(doAnchor bool, data string) {
+func (window *Window) print(doAnchor bool, data string) {
window.lock.Lock()
defer window.lock.Unlock()
defer cursor.Bottom()
@@ -132,13 +132,13 @@ func (window *window) print(doAnchor bool, data string) {
fmt.Print(data)
}
-func (window *window) Reads(label string, a ...interface{}) (value string) {
+func (window *Window) Reads(label string, a ...interface{}) (value string) {
window.lock.Lock()
defer window.lock.Unlock()
defer cursor.Bottom()
window.shift(cursorDefault)
fmt.Printf(label+" ", a...)
- value, _ = bufio.NewReader(os.Stdin).ReadString('\n')
+ value = util.ErrWrap("")(bufio.NewReader(os.Stdin).ReadString('\n'))
value = strings.TrimSpace(value)
value = strings.Trim(value, "\n")
value = strings.Trim(value, "\r")
diff --git a/util/anchor/window_test.go b/util/anchor/window_test.go
index a0cf967..8bcec06 100644
--- a/util/anchor/window_test.go
+++ b/util/anchor/window_test.go
@@ -35,7 +35,7 @@ func TestWindow(t *testing.T) {
os.Stdin = stdinFile
var (
- window = Window(Normal)
+ window = New(Normal)
lot = window.Lot("lot")
)
lot.Printf("lot text 1")
diff --git a/util/char.go b/util/char.go
index ff2cdee..7bcea55 100644
--- a/util/char.go
+++ b/util/char.go
@@ -1,7 +1,5 @@
package util
-import "math/rand"
-
func RandomAlpha() rune {
- return rune('a' - 1 + rand.Intn(26))
+ return rune('a' - 1 + RandomInt(26))
}
diff --git a/util/cmd/ffmpeg.go b/util/cmd/ffmpeg.go
index d1f99b5..11dcd18 100644
--- a/util/cmd/ffmpeg.go
+++ b/util/cmd/ffmpeg.go
@@ -57,7 +57,7 @@ func (FFmpegCmd) VolumeAdd(path string, delta float64) error {
var (
output bytes.Buffer
temp = util.FileBaseStem(path) + ".norm" + filepath.Ext(path)
- cmd = exec.Command("ffmpeg",
+ cmd = exec.Command("ffmpeg", // nolint:gosec
"-i", path,
"-af", fmt.Sprintf("volume=%.1fdB", math.Abs(delta)),
"-y", temp,
diff --git a/util/error.go b/util/error.go
index e4aa70f..fc63b65 100644
--- a/util/error.go
+++ b/util/error.go
@@ -1,5 +1,8 @@
package util
+func ErrSuppress(_ error) {
+}
+
func ErrWrap[T any](def T) func(T, error) T {
return func(value T, err error) T {
if err != nil {
diff --git a/util/int.go b/util/int.go
index 92d0ab7..6c46966 100644
--- a/util/int.go
+++ b/util/int.go
@@ -1,8 +1,11 @@
package util
-import "math/rand"
+import (
+ "crypto/rand"
+ "math/big"
+)
func RandomInt(max int, mins ...int) int {
min := First(mins, 0)
- return rand.Intn(max-min) + min
+ return int(ErrWrap(big.NewInt(0))(rand.Int(rand.Reader, big.NewInt(int64(max-min)))).Int64()) + min
}
diff --git a/util/io.go b/util/io.go
index 90e4b72..1fd8cce 100644
--- a/util/io.go
+++ b/util/io.go
@@ -23,7 +23,7 @@ func FileMoveOrCopy(source, destination string, overwrite ...bool) error {
return err
}
- if err := os.WriteFile(destination, input, 0o644); err != nil {
+ if err := os.WriteFile(destination, input, 0o600); err != nil {
return err
}