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
6 changes: 5 additions & 1 deletion api-spec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,18 @@ components:
paths:
/:
get:
description: Returns an empty object, used to very API is reachable
description: Returns API reachability and playback readiness
responses:
200:
description: API is ok
content:
application/json:
schema:
type: object
properties:
playback_ready:
description: Whether the daemon is fully bootstrapped and ready to accept /player/play
type: boolean
/status:
get:
description: Returns the player status
Expand Down
14 changes: 12 additions & 2 deletions cmd/daemon/api_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ var (
type ApiRequestType string

const (
ApiRequestTypeRoot ApiRequestType = "root"
ApiRequestTypeWebApi ApiRequestType = "web_api"
ApiRequestTypeStatus ApiRequestType = "status"
ApiRequestTypeResume ApiRequestType = "resume"
Expand Down Expand Up @@ -89,6 +90,7 @@ const (
ApiEventTypeRepeatTrack ApiEventType = "repeat_track"
ApiEventTypeRepeatContext ApiEventType = "repeat_context"
ApiEventTypeShuffleContext ApiEventType = "shuffle_context"
ApiEventTypePlaybackReady ApiEventType = "playback_ready"
)

type ApiRequest struct {
Expand Down Expand Up @@ -251,6 +253,10 @@ type ApiResponseStatus struct {
Track *ApiResponseStatusTrack `json:"track"`
}

type ApiResponseRoot struct {
PlaybackReady bool `json:"playback_ready"`
}

type ApiResponseVolume struct {
Value uint32 `json:"value"`
Max uint32 `json:"max"`
Expand Down Expand Up @@ -407,8 +413,12 @@ func jsonDecode(r *http.Request, v any) error {
func (s *ConcreteApiServer) serve() {
m := http.NewServeMux()
m.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte("{}"))
if r.Method != "GET" {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}

s.handleRequest(ApiRequest{Type: ApiRequestTypeRoot}, w)
})
m.Handle("/web-api/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
s.handleRequest(ApiRequest{
Expand Down
17 changes: 11 additions & 6 deletions cmd/daemon/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,12 @@ func NewApp(cfg *Config) (app *App, err error) {

func (app *App) newAppPlayer(ctx context.Context, creds any) (_ *AppPlayer, err error) {
appPlayer := &AppPlayer{
app: app,
stop: make(chan struct{}, 1),
logout: app.logoutCh,
countryCode: new(string),
volumeUpdate: make(chan float32, 1),
app: app,
stop: make(chan struct{}, 1),
logout: app.logoutCh,
countryCode: new(string),
volumeUpdate: make(chan float32, 1),
playbackReadyCh: make(chan struct{}),
}

appPlayer.prefetchTimer = time.NewTimer(math.MaxInt64)
Expand Down Expand Up @@ -283,7 +284,11 @@ func (app *App) withAppPlayer(ctx context.Context, appPlayerFunc func(context.Co
select {
case req := <-app.server.Receive():
if currentPlayer == nil {
req.Reply(nil, ErrNoSession)
if req.Type == ApiRequestTypeRoot {
req.Reply(&ApiResponseRoot{}, nil)
} else {
req.Reply(nil, ErrNoSession)
}
break
}

Expand Down
34 changes: 34 additions & 0 deletions cmd/daemon/player.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,39 @@ type AppPlayer struct {
prodInfo *ProductInfo
countryCode *string

hasSpotConnId bool
hasInitialConnectState bool
hasCountryCode bool
playbackReadyCh chan struct{}
playbackReadyOnce sync.Once

state *State
primaryStream *player.Stream
secondaryStream *player.Stream

prefetchTimer *time.Timer
}

func (p *AppPlayer) playbackReady() bool {
select {
case <-p.playbackReadyCh:
return true
default:
return false
}
}

func (p *AppPlayer) notifyPlaybackReadyIfNeeded() {
if !p.hasSpotConnId || !p.hasInitialConnectState || !p.hasCountryCode {
return
}

p.playbackReadyOnce.Do(func() {
close(p.playbackReadyCh)
p.app.server.Emit(&ApiEvent{Type: ApiEventTypePlaybackReady})
})
}

func (p *AppPlayer) handleAccesspointPacket(pktType ap.PacketType, payload []byte) error {
switch pktType {
case ap.PacketTypeProductInfo:
Expand All @@ -67,6 +93,8 @@ func (p *AppPlayer) handleAccesspointPacket(pktType ap.PacketType, payload []byt
return nil
case ap.PacketTypeCountryCode:
*p.countryCode = string(payload)
p.hasCountryCode = true
p.notifyPlaybackReadyIfNeeded()
return nil
default:
return nil
Expand All @@ -80,13 +108,17 @@ func (p *AppPlayer) handleDealerMessage(ctx context.Context, msg dealer.Message)

if strings.HasPrefix(msg.Uri, "hm://pusher/v1/connections/") {
p.spotConnId = msg.Headers["Spotify-Connection-Id"]
p.hasSpotConnId = p.spotConnId != ""
p.app.log.Debugf("received connection id: %s...%s", p.spotConnId[:16], p.spotConnId[len(p.spotConnId)-16:])

// put the initial state
if err := p.putConnectState(ctx, connectpb.PutStateReason_NEW_DEVICE); err != nil {
return fmt.Errorf("failed initial state put: %w", err)
}

p.hasInitialConnectState = true
p.notifyPlaybackReadyIfNeeded()

if !p.app.cfg.ExternalVolume && len(p.app.cfg.MixerDevice) == 0 {
// update initial volume
p.initialVolumeOnce.Do(func() {
Expand Down Expand Up @@ -377,6 +409,8 @@ func (p *AppPlayer) handleApiRequest(ctx context.Context, req ApiRequest) (any,
defer cancel()

switch req.Type {
case ApiRequestTypeRoot:
return &ApiResponseRoot{PlaybackReady: p.playbackReady()}, nil
case ApiRequestTypeWebApi:
data := req.Data.(ApiRequestDataWebApi)
resp, err := p.sess.WebApi(ctx, data.Method, data.Path, data.Query, nil, nil)
Expand Down
Loading