spogo/internal/spotify/applescript.go
2026-01-07 09:09:42 +01:00

301 lines
8.2 KiB
Go

//go:build darwin
// +build darwin
package spotify
import (
"context"
"fmt"
"os/exec"
"strconv"
"strings"
)
type AppleScriptClient struct {
fallback API
}
type AppleScriptOptions struct {
Fallback API
}
func NewAppleScriptClient(opts AppleScriptOptions) (API, error) {
return &AppleScriptClient{
fallback: opts.Fallback,
}, nil
}
func (c *AppleScriptClient) runScript(ctx context.Context, script string) (string, error) {
cmd := exec.CommandContext(ctx, "osascript", "-e", script)
out, err := cmd.CombinedOutput()
if err != nil {
msg := strings.TrimSpace(string(out))
if msg == "" {
return "", fmt.Errorf("applescript error: %w", err)
}
return "", fmt.Errorf("applescript error: %w (%s)", err, msg)
}
return strings.TrimSpace(string(out)), nil
}
func (c *AppleScriptClient) Play(ctx context.Context, uri string) error {
var script string
if uri == "" {
script = `tell application "Spotify" to play`
} else {
script = fmt.Sprintf(`tell application "Spotify" to play track "%s"`, uri)
}
_, err := c.runScript(ctx, script)
return err
}
func (c *AppleScriptClient) Pause(ctx context.Context) error {
_, err := c.runScript(ctx, `tell application "Spotify" to pause`)
return err
}
func (c *AppleScriptClient) Next(ctx context.Context) error {
_, err := c.runScript(ctx, `tell application "Spotify" to next track`)
return err
}
func (c *AppleScriptClient) Previous(ctx context.Context) error {
_, err := c.runScript(ctx, `tell application "Spotify" to previous track`)
return err
}
func (c *AppleScriptClient) Seek(ctx context.Context, positionMS int) error {
positionSec := positionMS / 1000
script := fmt.Sprintf(`tell application "Spotify" to set player position to %d`, positionSec)
_, err := c.runScript(ctx, script)
return err
}
func (c *AppleScriptClient) Volume(ctx context.Context, volume int) error {
script := fmt.Sprintf(`tell application "Spotify" to set sound volume to %d`, volume)
_, err := c.runScript(ctx, script)
return err
}
func (c *AppleScriptClient) Shuffle(ctx context.Context, enabled bool) error {
val := "false"
if enabled {
val = "true"
}
script := fmt.Sprintf(`tell application "Spotify" to set shuffling to %s`, val)
_, err := c.runScript(ctx, script)
return err
}
func (c *AppleScriptClient) Repeat(ctx context.Context, mode string) error {
val := "false"
if mode == "track" || mode == "context" {
val = "true"
}
script := fmt.Sprintf(`tell application "Spotify" to set repeating to %s`, val)
_, err := c.runScript(ctx, script)
return err
}
func (c *AppleScriptClient) Playback(ctx context.Context) (PlaybackStatus, error) {
script := `tell application "Spotify"
set trackName to name of current track
set trackArtist to artist of current track
set trackAlbum to album of current track
set trackID to id of current track
set trackDuration to duration of current track
set playerPos to player position
set playerState to player state as string
set vol to sound volume
set isShuffling to shuffling
set isRepeating to repeating
return trackName & "|||" & trackArtist & "|||" & trackAlbum & "|||" & trackID & "|||" & trackDuration & "|||" & playerPos & "|||" & playerState & "|||" & vol & "|||" & isShuffling & "|||" & isRepeating
end tell`
out, err := c.runScript(ctx, script)
if err != nil {
return PlaybackStatus{}, err
}
parts := strings.Split(out, "|||")
if len(parts) < 10 {
return PlaybackStatus{}, fmt.Errorf("unexpected applescript output: %s", out)
}
durationMS, _ := strconv.Atoi(parts[4])
positionSec, _ := strconv.ParseFloat(parts[5], 64)
volume, _ := strconv.Atoi(parts[7])
isPlaying := parts[6] == "playing"
shuffle := parts[8] == "true"
repeat := "off"
if parts[9] == "true" {
repeat = "context"
}
item := &Item{
URI: parts[3],
Name: parts[0],
Artists: []string{parts[1]},
Album: parts[2],
DurationMS: durationMS,
}
return PlaybackStatus{
IsPlaying: isPlaying,
ProgressMS: int(positionSec * 1000),
Item: item,
Device: Device{
ID: "local",
Name: "Local Spotify",
Type: "COMPUTER",
Volume: volume,
Active: true,
},
Shuffle: shuffle,
Repeat: repeat,
}, nil
}
func (c *AppleScriptClient) Devices(ctx context.Context) ([]Device, error) {
return []Device{
{
ID: "local",
Name: "Local Spotify",
Type: "COMPUTER",
Active: true,
},
}, nil
}
func (c *AppleScriptClient) Transfer(ctx context.Context, deviceID string) error {
return ErrUnsupported
}
func (c *AppleScriptClient) QueueAdd(ctx context.Context, uri string) error {
if c.fallback != nil {
return c.fallback.QueueAdd(ctx, uri)
}
return ErrUnsupported
}
func (c *AppleScriptClient) Queue(ctx context.Context) (Queue, error) {
if c.fallback != nil {
return c.fallback.Queue(ctx)
}
return Queue{}, ErrUnsupported
}
func (c *AppleScriptClient) Search(ctx context.Context, kind, query string, limit, offset int) (SearchResult, error) {
if c.fallback != nil {
return c.fallback.Search(ctx, kind, query, limit, offset)
}
return SearchResult{}, ErrUnsupported
}
func (c *AppleScriptClient) GetTrack(ctx context.Context, id string) (Item, error) {
if c.fallback != nil {
return c.fallback.GetTrack(ctx, id)
}
return Item{}, ErrUnsupported
}
func (c *AppleScriptClient) GetAlbum(ctx context.Context, id string) (Item, error) {
if c.fallback != nil {
return c.fallback.GetAlbum(ctx, id)
}
return Item{}, ErrUnsupported
}
func (c *AppleScriptClient) GetArtist(ctx context.Context, id string) (Item, error) {
if c.fallback != nil {
return c.fallback.GetArtist(ctx, id)
}
return Item{}, ErrUnsupported
}
func (c *AppleScriptClient) GetPlaylist(ctx context.Context, id string) (Item, error) {
if c.fallback != nil {
return c.fallback.GetPlaylist(ctx, id)
}
return Item{}, ErrUnsupported
}
func (c *AppleScriptClient) GetShow(ctx context.Context, id string) (Item, error) {
if c.fallback != nil {
return c.fallback.GetShow(ctx, id)
}
return Item{}, ErrUnsupported
}
func (c *AppleScriptClient) GetEpisode(ctx context.Context, id string) (Item, error) {
if c.fallback != nil {
return c.fallback.GetEpisode(ctx, id)
}
return Item{}, ErrUnsupported
}
func (c *AppleScriptClient) LibraryTracks(ctx context.Context, limit, offset int) ([]Item, int, error) {
if c.fallback != nil {
return c.fallback.LibraryTracks(ctx, limit, offset)
}
return nil, 0, ErrUnsupported
}
func (c *AppleScriptClient) LibraryAlbums(ctx context.Context, limit, offset int) ([]Item, int, error) {
if c.fallback != nil {
return c.fallback.LibraryAlbums(ctx, limit, offset)
}
return nil, 0, ErrUnsupported
}
func (c *AppleScriptClient) LibraryModify(ctx context.Context, path string, ids []string, method string) error {
if c.fallback != nil {
return c.fallback.LibraryModify(ctx, path, ids, method)
}
return ErrUnsupported
}
func (c *AppleScriptClient) FollowArtists(ctx context.Context, ids []string, method string) error {
if c.fallback != nil {
return c.fallback.FollowArtists(ctx, ids, method)
}
return ErrUnsupported
}
func (c *AppleScriptClient) FollowedArtists(ctx context.Context, limit int, after string) ([]Item, int, string, error) {
if c.fallback != nil {
return c.fallback.FollowedArtists(ctx, limit, after)
}
return nil, 0, "", ErrUnsupported
}
func (c *AppleScriptClient) Playlists(ctx context.Context, limit, offset int) ([]Item, int, error) {
if c.fallback != nil {
return c.fallback.Playlists(ctx, limit, offset)
}
return nil, 0, ErrUnsupported
}
func (c *AppleScriptClient) PlaylistTracks(ctx context.Context, id string, limit, offset int) ([]Item, int, error) {
if c.fallback != nil {
return c.fallback.PlaylistTracks(ctx, id, limit, offset)
}
return nil, 0, ErrUnsupported
}
func (c *AppleScriptClient) CreatePlaylist(ctx context.Context, name string, public, collaborative bool) (Item, error) {
if c.fallback != nil {
return c.fallback.CreatePlaylist(ctx, name, public, collaborative)
}
return Item{}, ErrUnsupported
}
func (c *AppleScriptClient) AddTracks(ctx context.Context, playlistID string, uris []string) error {
if c.fallback != nil {
return c.fallback.AddTracks(ctx, playlistID, uris)
}
return ErrUnsupported
}
func (c *AppleScriptClient) RemoveTracks(ctx context.Context, playlistID string, uris []string) error {
if c.fallback != nil {
return c.fallback.RemoveTracks(ctx, playlistID, uris)
}
return ErrUnsupported
}