diff --git a/CHANGELOG.md b/CHANGELOG.md index dde876d..ae05a3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `--album` flag to track command in CLI mode - Optional album field in TUI mode for track scrobbling - Album validation - when user provides `--album`, validates it's a valid release for the track via Last.fm +- Dryrun now shows CLI command to reproduce scrobble +- Option to copy CLI command to clipboard in dryrun mode (TUI mode only) ### Changed - Output now displays album name in parentheses for both track and album scrobbles @@ -25,6 +27,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Duplicate error output fixed - Consistent usage display for arg validation errors +### Documentation +- Added CONTRIBUTING.md with contribution guidelines and release process +- Added note about main branch representing active development + ## [0.1.1] - 2024-05-15 ### Added @@ -50,4 +56,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Form validation for empty fields ### Removed -- Debug flag and logging in release builds \ No newline at end of file +- Debug flag and logging in release builds diff --git a/README.md b/README.md index 2a0bac8..b301d49 100644 --- a/README.md +++ b/README.md @@ -42,13 +42,13 @@ Linux Secret Service). Profile metadata is stored at `~/.config/spin/`. ### Scrobble Modes -- 👤 **TUI mode**: An interactive mode that prompts for artist and release details, - searches last.fm for the best match, and scrobbles automatically. TUI mode closes - automatically when a scrobble is submitted, or can be closed with `Ctrl+C`. +- 👤 **TUI mode**: An interactive mode that prompts for artist and release + details to scrobble. TUI mode closes automatically when a scrobble is + submitted, or can be quit with `Ctrl+C`. - 🤖 **CLI mode**: An automation friendly mode that scrobbles tracks and albums - directly using details provided as command arguments. Scrobbles are submitted as - soon as the command is run. + directly using details provided as command arguments. Scrobbles are submitted + as soon as the command is run. By default, Spin uses the current time for scrobbles. However, both modes provide ways to set custom timestamps. @@ -122,15 +122,17 @@ spin track # interactively search for and scrobble a track spin album # interactively search for and scrobble an album ``` -In addition to prompting for the `artist`, `track`/`album`, and (for tracks) optionally the -`album`, TUI mode also allows you to specify the scrobble date and time: +In addition to prompting for the `artist`, `track`/`album`, and (for tracks) +optionally the `album`, TUI mode also allows you to specify the scrobble date +and time: - **Starting now**: Scrobble at the current time - **Ending now**: Calculate start time from track/album duration - **Custom start time**: Provide a specific date and time Available TUI mode options: - `-p|--profile`: profile to scrobble with (default: active profile) -- `--dryrun`: show what would be scrobbled without submitting +- `--dryrun`: show what would be scrobbled without submitting (prompts to copy + CLI command to easily reproduce) #### CLI mode @@ -185,8 +187,10 @@ spin history -n 50 # set the number of results Spin was inspired by these projects: -- [scrobbler](https://github.com/hauzer/scrobbler) - A simple CLI Last.fm scrobbler -- [OpenWebScrobbler](https://github.com/elamperti/OpenWebScrobbler) - A web-based scrobbler +- [scrobbler](https://github.com/hauzer/scrobbler) - A simple CLI Last.fm + scrobbler +- [OpenWebScrobbler](https://github.com/elamperti/OpenWebScrobbler) - A + web-based scrobbler ## License diff --git a/cmd/album.go b/cmd/album.go index ddea5da..e2042c4 100644 --- a/cmd/album.go +++ b/cmd/album.go @@ -46,6 +46,7 @@ type scrobbleInput struct { Timestamp time.Time Profile string Dryrun bool + TUIMode bool } var albumCmd = &cobra.Command{ @@ -67,8 +68,10 @@ If artist and album are provided, scrobbles directly (CLI mode).`, var artist, name string var timeMode scrobble.TimeMode var customDate, customTime string + tuiMode := false if len(args) == 0 { + tuiMode = true input, err := tui.CollectAlbumInput() if err != nil { return err @@ -124,6 +127,7 @@ If artist and album are provided, scrobbles directly (CLI mode).`, Timestamp: timestamp, Profile: profileFlag, Dryrun: dryrun, + TUIMode: tuiMode, } return scrobbleAlbum(input) @@ -145,6 +149,7 @@ func scrobbleAlbum(input *scrobbleInput) error { } if input.Dryrun { + cliCmd := buildAlbumCLICommand(input.Artist, input.Album, input.Timestamp) fmt.Printf("Would scrobble to %s:\n\n", username) currentTs := input.Timestamp for i, track := range input.Tracks { @@ -153,6 +158,17 @@ func scrobbleAlbum(input *scrobbleInput) error { fmt.Printf("%2d. %s - %s%*s (%s) (%s)\n", i+1, input.Artist, track.Name, maxWidth-nameWidth, "", input.Album, tsFormatted) currentTs = currentTs.Add(time.Duration(track.Duration) * time.Second) } + + fmt.Printf("\nRun this command to scrobble:\n %s\n\n", cliCmd) + + if input.TUIMode && askCopyToClipboard() { + if err := copyToClipboard(cliCmd); err != nil { + fmt.Fprintf(os.Stderr, "Failed to copy: %v\n", err) + } else { + fmt.Println("Command copied to clipboard!") + } + } + return nil } diff --git a/cmd/helpers.go b/cmd/helpers.go new file mode 100644 index 0000000..07d8e75 --- /dev/null +++ b/cmd/helpers.go @@ -0,0 +1,56 @@ +package cmd + +import ( + "fmt" + "os/exec" + "runtime" + "strings" + "time" +) + +func buildTrackCLICommand(artist, track, album string, timestamp time.Time) string { + cmd := fmt.Sprintf("spin track %q %q", artist, track) + + if album != "" { + cmd += fmt.Sprintf(" --album %q", album) + } + + cmd += fmt.Sprintf(" --timestamp %s", timestamp.Format("15:04")) + cmd += fmt.Sprintf(" --date %s", timestamp.Format("2006-01-02")) + + return cmd +} + +func buildAlbumCLICommand(artist, album string, timestamp time.Time) string { + cmd := fmt.Sprintf("spin album %q %q", artist, album) + + cmd += fmt.Sprintf(" --timestamp %s", timestamp.Format("15:04")) + cmd += fmt.Sprintf(" --date %s", timestamp.Format("2006-01-02")) + + return cmd +} + +func askCopyToClipboard() bool { + fmt.Print("Copy command to clipboard? [y/N] ") + var response string + fmt.Scanln(&response) + return response == "y" || response == "Y" +} + +func copyToClipboard(text string) error { + var cmd *exec.Cmd + + switch runtime.GOOS { + case "darwin": + cmd = exec.Command("pbcopy") + case "linux": + cmd = exec.Command("bash", "-c", "echo -n "+fmt.Sprintf("%q", text)+" | xclip -selection clipboard") + case "windows": + cmd = exec.Command("cmd", "/c", "echo "+text+"| clip") + default: + return fmt.Errorf("unsupported platform") + } + + cmd.Stdin = strings.NewReader(text) + return cmd.Run() +} diff --git a/cmd/track.go b/cmd/track.go index c0dc0b4..d1ff1c5 100644 --- a/cmd/track.go +++ b/cmd/track.go @@ -26,14 +26,15 @@ If artist and track are provided, scrobbles directly (CLI mode).`, endNow, _ := cmd.Flags().GetBool("end-now") dateFlag, _ := cmd.Flags().GetString("date") timestampFlag, _ := cmd.Flags().GetString("timestamp") - albumFlag, _ := cmd.Flags().GetString("album") dryrun, _ := cmd.Flags().GetBool("dryrun") var artist, track, album string var timeMode scrobble.TimeMode var customDate, customTime string + tuiMode := false if len(args) == 0 { + tuiMode = true input, err := tui.CollectTrackInput() if err != nil { return err @@ -55,6 +56,7 @@ If artist and track are provided, scrobbles directly (CLI mode).`, } artist = args[0] track = args[1] + albumFlag, _ := cmd.Flags().GetString("album") album = albumFlag if endNow { @@ -103,7 +105,7 @@ If artist and track are provided, scrobbles directly (CLI mode).`, } } - return scrobbleTrack(artist, track, albumName, timestamp, profileFlag, dryrun) + return scrobbleTrack(artist, track, albumName, timestamp, profileFlag, dryrun, tuiMode) }, } @@ -115,7 +117,7 @@ func printTrack(artist, track, album, timestamp string) { } } -func scrobbleTrack(artist, track, album string, timestamp time.Time, profileFlag string, dryrun bool) error { +func scrobbleTrack(artist, track, album string, timestamp time.Time, profileFlag string, dryrun bool, tuiMode bool) error { ts := scrobble.FormatTimestamp(timestamp) tsFormatted := timestamp.Format("2006-01-02 15:04") @@ -125,8 +127,20 @@ func scrobbleTrack(artist, track, album string, timestamp time.Time, profileFlag } if dryrun { + cliCmd := buildTrackCLICommand(artist, track, album, timestamp) fmt.Printf("Would scrobble to %s:\n\n", username) printTrack(artist, track, album, tsFormatted) + + fmt.Printf("\nRun this command to scrobble:\n %s\n\n", cliCmd) + + if tuiMode && askCopyToClipboard() { + if err := copyToClipboard(cliCmd); err != nil { + fmt.Fprintf(os.Stderr, "Failed to copy: %v\n", err) + } else { + fmt.Println("Command copied to clipboard!") + } + } + return nil }