145 lines
4.1 KiB
Go
145 lines
4.1 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gorilla/websocket"
|
|
)
|
|
|
|
type request struct {
|
|
Name string `json:"name"`
|
|
Arguments struct {
|
|
URI string `json:"uri"`
|
|
Action string `json:"action"`
|
|
Volume *int `json:"volume"`
|
|
Active bool `json:"active"`
|
|
} `json:"arguments"`
|
|
}
|
|
|
|
var validURI = regexp.MustCompile(`^spotify:(track|artist|album|playlist|episode):[A-Za-z0-9]{22}$`)
|
|
|
|
func execute(req request) (map[string]interface{}, error) {
|
|
if req.Name == "duck" {
|
|
volume := "100%"
|
|
if req.Arguments.Active {
|
|
volume = "75%"
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
cmd := exec.CommandContext(ctx, "/data/opt/alice-backend/spotify/pactl", "set-sink-volume", "music", volume)
|
|
if err := cmd.Run(); err != nil {
|
|
return nil, errors.New("music_audio_unavailable")
|
|
}
|
|
return map[string]interface{}{"ok": true}, nil
|
|
}
|
|
|
|
command := map[string]interface{}{"type": "command"}
|
|
if req.Name == "spotify_play" {
|
|
if !validURI.MatchString(req.Arguments.URI) {
|
|
return nil, errors.New("invalid_spotify_uri")
|
|
}
|
|
command["command"] = "play"
|
|
command["uri"] = req.Arguments.URI
|
|
} else if req.Name == "spotify_control" {
|
|
actions := map[string]string{"pause": "pause", "resume": "play", "next": "skip_next", "previous": "skip_prev", "volume": "set_volume"}
|
|
action, ok := actions[req.Arguments.Action]
|
|
if !ok {
|
|
return nil, errors.New("invalid_music_action")
|
|
}
|
|
command["command"] = action
|
|
if action == "set_volume" {
|
|
if req.Arguments.Volume == nil || *req.Arguments.Volume < 0 || *req.Arguments.Volume > 100 {
|
|
return nil, errors.New("invalid_music_volume")
|
|
}
|
|
command["volume"] = *req.Arguments.Volume
|
|
}
|
|
} else {
|
|
return nil, errors.New("unknown_music_tool")
|
|
}
|
|
dialer := websocket.Dialer{HandshakeTimeout: 2 * time.Second}
|
|
conn, _, err := dialer.Dial("ws://127.0.0.1:9090", nil)
|
|
if err != nil {
|
|
return nil, errors.New("spotify_unavailable")
|
|
}
|
|
defer conn.Close()
|
|
conn.SetReadLimit(256 * 1024)
|
|
deadline := time.Now().Add(10 * time.Second)
|
|
_ = conn.SetReadDeadline(deadline)
|
|
_ = conn.SetWriteDeadline(deadline)
|
|
var auth struct {
|
|
Type string `json:"type"`
|
|
LoggedIn bool `json:"logged_in"`
|
|
}
|
|
if err := conn.ReadJSON(&auth); err != nil || auth.Type != "auth_state" || !auth.LoggedIn {
|
|
return nil, errors.New("spotify_not_paired")
|
|
}
|
|
if err := conn.WriteJSON(command); err != nil {
|
|
return nil, errors.New("spotify_disconnected")
|
|
}
|
|
accepted := false
|
|
for {
|
|
var event map[string]interface{}
|
|
if err := conn.ReadJSON(&event); err != nil {
|
|
if accepted {
|
|
return map[string]interface{}{"accepted": true, "status": "pending"}, nil
|
|
}
|
|
return nil, errors.New("spotify_response_timeout")
|
|
}
|
|
switch event["type"] {
|
|
case "error":
|
|
return nil, errors.New("spotify_command_failed")
|
|
case "command_result":
|
|
if event["command"] == command["command"] {
|
|
accepted = true
|
|
if err := conn.WriteJSON(map[string]string{"type": "command", "command": "get_state"}); err != nil {
|
|
return nil, errors.New("spotify_disconnected")
|
|
}
|
|
}
|
|
case "playback_state":
|
|
if accepted {
|
|
return map[string]interface{}{"accepted": true, "status": event["status"], "item": event["item"], "volume": event["volume"]}, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
cacheDir := flag.String("cache-janitor", "", "maintain the dedicated Soloist cache directory")
|
|
flag.Parse()
|
|
if *cacheDir != "" {
|
|
if err := runCacheJanitor(*cacheDir); err != nil {
|
|
fmt.Fprintln(os.Stderr, "spotify cache:", err)
|
|
os.Exit(1)
|
|
}
|
|
return
|
|
}
|
|
var req request
|
|
decoder := json.NewDecoder(io.LimitReader(os.Stdin, 8193))
|
|
decoder.DisallowUnknownFields()
|
|
var result map[string]interface{}
|
|
if err := decoder.Decode(&req); err != nil {
|
|
result = map[string]interface{}{"error": "invalid_music_request"}
|
|
} else {
|
|
var err error
|
|
result, err = execute(req)
|
|
if err != nil {
|
|
result = map[string]interface{}{"error": strings.TrimSpace(err.Error())}
|
|
}
|
|
}
|
|
data, err := json.Marshal(result)
|
|
if err != nil || len(data) > 12000 {
|
|
data = []byte(`{"error":"music_result_too_large"}`)
|
|
}
|
|
fmt.Println(string(data))
|
|
}
|