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 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 }