Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
- Debug flag and logging in release builds
24 changes: 14 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
16 changes: 16 additions & 0 deletions cmd/album.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ type scrobbleInput struct {
Timestamp time.Time
Profile string
Dryrun bool
TUIMode bool
}

var albumCmd = &cobra.Command{
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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 {
Expand All @@ -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
}

Expand Down
56 changes: 56 additions & 0 deletions cmd/helpers.go
Original file line number Diff line number Diff line change
@@ -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()
}
20 changes: 17 additions & 3 deletions cmd/track.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
},
}

Expand All @@ -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")

Expand All @@ -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
}

Expand Down
Loading