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
16 changes: 16 additions & 0 deletions api-spec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,22 @@ paths:
type: boolean
track:
$ref: '#/components/schemas/track'
/set_device_name:
post:
description: Set the player device name
requestBody:
content:
application/json:
schema:
type: object
required: [ name ]
properties:
name:
description: The new device name
type: string
responses:
200:
description: Successful response
/player/play:
post:
description: Starts playing new content
Expand Down
22 changes: 22 additions & 0 deletions daemon/api_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ const (
ApiRequestTypeSetShufflingContext ApiRequestType = "shuffling_context"
ApiRequestTypeAddToQueue ApiRequestType = "add_to_queue"
ApiRequestTypeToken ApiRequestType = "token"
ApiRequestSetDeviceName ApiRequestType = "set_device_name"
)

type ApiEventType string
Expand Down Expand Up @@ -648,6 +649,27 @@ func (s *ConcreteApiServer) serve() {

s.handleRequest(ApiRequest{Type: ApiRequestTypeToken}, w)
})
m.HandleFunc("/set_device_name", func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}

var data struct {
Name string `json:"name"`
}
if err := jsonDecode(r, &data); err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}

if len(data.Name) == 0 {
w.WriteHeader(http.StatusBadRequest)
return
}

s.handleRequest(ApiRequest{Type: ApiRequestSetDeviceName, Data: data.Name}, w)
})
m.HandleFunc("/events", func(w http.ResponseWriter, r *http.Request) {
opts := &websocket.AcceptOptions{}
if len(s.allowOrigin) > 0 {
Expand Down
25 changes: 19 additions & 6 deletions daemon/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type App struct {
client *http.Client

resolver *apresolve.ApResolver
zeroconf *zeroconf.Zeroconf

deviceId string
deviceType devicespb.DeviceType
Expand Down Expand Up @@ -129,6 +130,18 @@ func New(opts *Options) (*App, error) {
return app, nil
}

func (app *App) SetDeviceName(name string) {
if app.cfg.DeviceName == name {
return
}

app.cfg.DeviceName = name

if app.zeroconf != nil {
app.zeroconf.SetDeviceName(name)
}
}

// Run starts the daemon. It blocks until ctx is cancelled or an unrecoverable
// error occurs. The credential type configured in cfg.Credentials.Type
// determines which login flow is used.
Expand Down Expand Up @@ -306,7 +319,7 @@ func (app *App) withAppPlayer(ctx context.Context, appPlayerFunc func(context.Co
return fmt.Errorf("failed getting endpoints from resolver: %w", err)
}

z, err := zeroconf.NewZeroconf(app.log, app.cfg.ZeroconfPort, app.cfg.DeviceName, app.deviceId, app.deviceType, app.cfg.ZeroconfInterfacesToAdvertise, app.cfg.ZeroconfBackend == "avahi")
app.zeroconf, err = zeroconf.NewZeroconf(app.log, app.cfg.ZeroconfPort, app.cfg.DeviceName, app.deviceId, app.deviceType, app.cfg.ZeroconfInterfacesToAdvertise, app.cfg.ZeroconfBackend == "avahi")
if err != nil {
return fmt.Errorf("failed initializing zeroconf: %w", err)
}
Expand All @@ -325,7 +338,7 @@ func (app *App) withAppPlayer(ctx context.Context, appPlayerFunc func(context.Co
apiCh = make(chan ApiRequest)
go currentPlayer.Run(ctx, apiCh, app.mpris.Receive())

z.SetCurrentUser(currentPlayer.sess.Username())
app.zeroconf.SetCurrentUser(currentPlayer.sess.Username())
}

go func() {
Expand Down Expand Up @@ -363,16 +376,16 @@ func (app *App) withAppPlayer(ctx context.Context, appPlayerFunc func(context.Co
if err != nil {
app.log.WithError(err).Errorf("failed restoring session after logout")

z.SetCurrentUser("")
app.zeroconf.SetCurrentUser("")
} else if newAppPlayer == nil {
z.SetCurrentUser("")
app.zeroconf.SetCurrentUser("")
} else {
apiCh = make(chan ApiRequest)
currentPlayer = newAppPlayer

go newAppPlayer.Run(ctx, apiCh, app.mpris.Receive())

z.SetCurrentUser(newAppPlayer.sess.Username())
app.zeroconf.SetCurrentUser(newAppPlayer.sess.Username())

app.log.WithField("username", librespot.ObfuscateUsername(currentPlayer.sess.Username())).
Debugf("restored session after logout")
Expand All @@ -381,7 +394,7 @@ func (app *App) withAppPlayer(ctx context.Context, appPlayerFunc func(context.Co
}
}()

return z.Serve(func(req zeroconf.NewUserRequest) bool {
return app.zeroconf.Serve(func(req zeroconf.NewUserRequest) bool {
if currentPlayer != nil {
currentPlayer.Close()
currentPlayer = nil
Expand Down
10 changes: 10 additions & 0 deletions daemon/player.go
Original file line number Diff line number Diff line change
Expand Up @@ -593,11 +593,21 @@ func (p *AppPlayer) handleApiRequest(ctx context.Context, req ApiRequest) (any,
return &ApiResponseToken{
Token: accessToken,
}, nil
case ApiRequestSetDeviceName:
p.setDeviceName(ctx, req.Data.(string))
return nil, nil
default:
return nil, fmt.Errorf("unknown request type: %s", req.Type)
}
}

func (p *AppPlayer) setDeviceName(ctx context.Context, name string) {
p.app.SetDeviceName(name)

p.state.device.Name = name
p.updateState(ctx)
}

func pointer[T any](d T) *T {
return &d
}
Expand Down
3 changes: 3 additions & 0 deletions zeroconf/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ type ServiceRegistrar interface {
// txt: TXT record key=value pairs
Register(name, serviceType, domain string, port int, txt []string) error

// UpdateName updates teh service instance name.
UpdateName(name string) error

// Shutdown stops advertising the service and releases resources.
Shutdown()
}
47 changes: 33 additions & 14 deletions zeroconf/backend_avahi.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,14 @@ const (
// Compatibility: Requires avahi-daemon 0.6.x or later (uses stable D-Bus API).
// Tested with avahi 0.7 and 0.8.
type AvahiRegistrar struct {
conn *dbus.Conn
entryGroup dbus.BusObject
version string
conn *dbus.Conn
version string

entryGroup dbus.BusObject
serviceType string
domain string
port int
txt []string
}

// NewAvahiRegistrar creates a new avahi-daemon service registrar.
Expand Down Expand Up @@ -80,14 +85,16 @@ func (a *AvahiRegistrar) Register(name, serviceType, domain string, port int, tx
server := a.conn.Object(avahiService, avahiServerPath)

// Create a new entry group for our service
var groupPath dbus.ObjectPath
err := server.Call(avahiServerIface+".EntryGroupNew", 0).Store(&groupPath)
if err != nil {
return fmt.Errorf("failed to create entry group: %w", err)
if a.entryGroup == nil {
var groupPath dbus.ObjectPath
err := server.Call(avahiServerIface+".EntryGroupNew", 0).Store(&groupPath)
if err != nil {
return fmt.Errorf("failed to create entry group: %w", err)
}

a.entryGroup = a.conn.Object(avahiService, groupPath)
}

a.entryGroup = a.conn.Object(avahiService, groupPath)

// Convert TXT records to [][]byte format required by avahi
txtBytes := make([][]byte, len(txt))
for i, t := range txt {
Expand All @@ -104,7 +111,7 @@ func (a *AvahiRegistrar) Register(name, serviceType, domain string, port int, tx
// host (s): hostname, empty for default
// port (q): port number (uint16)
// txt (aay): TXT record data as array of byte arrays
err = a.entryGroup.Call(avahiEntryGroupIface+".AddService", 0,
if err := a.entryGroup.Call(avahiEntryGroupIface+".AddService", 0,
avahiIfUnspec, // interface
avahiProtoUnspec, // protocol
uint32(0), // flags
Expand All @@ -114,20 +121,32 @@ func (a *AvahiRegistrar) Register(name, serviceType, domain string, port int, tx
"", // host (empty = use default hostname)
uint16(port), // port
txtBytes, // TXT records
).Err
if err != nil {
).Err; err != nil {
return fmt.Errorf("failed to add service: %w", err)
}

a.serviceType = serviceType
a.domain = domain
a.port = port
a.txt = txt

// Commit the entry group to publish the service
err = a.entryGroup.Call(avahiEntryGroupIface+".Commit", 0).Err
if err != nil {
if err := a.entryGroup.Call(avahiEntryGroupIface+".Commit", 0).Err; err != nil {
return fmt.Errorf("failed to commit entry group: %w", err)
}

return nil
}

// UpdateName updates the advertised instance service name.
func (a *AvahiRegistrar) UpdateName(name string) error {
if a.entryGroup != nil {
_ = a.entryGroup.Call(avahiEntryGroupIface+".Reset", 0).Err
}

return a.Register(name, a.serviceType, a.domain, a.port, a.txt)
}

// Shutdown removes the service from avahi and releases resources.
func (a *AvahiRegistrar) Shutdown() {
if a.entryGroup != nil {
Expand Down
16 changes: 16 additions & 0 deletions zeroconf/backend_builtin.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ import (
type BuiltinRegistrar struct {
server *zeroconf.Server
ifaces []net.Interface

serviceType string
domain string
port int
txt []string
}

// NewBuiltinRegistrar creates a new built-in mDNS service registrar.
Expand All @@ -21,11 +26,22 @@ func NewBuiltinRegistrar(ifaces []net.Interface) *BuiltinRegistrar {

// Register publishes the service using the built-in mDNS responder.
func (b *BuiltinRegistrar) Register(name, serviceType, domain string, port int, txt []string) error {
b.serviceType = serviceType
b.domain = domain
b.port = port
b.txt = txt

var err error
b.server, err = zeroconf.Register(name, serviceType, domain, port, txt, b.ifaces)
return err
}

func (b *BuiltinRegistrar) UpdateName(name string) error {
// Zeroconf library does not support dynamic updates, so we need to restart the server with the new name.
b.Shutdown()
return b.Register(name, b.serviceType, b.domain, b.port, b.txt)
}

// Shutdown stops the mDNS responder.
func (b *BuiltinRegistrar) Shutdown() {
if b.server != nil {
Expand Down
8 changes: 8 additions & 0 deletions zeroconf/zeroconf.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,14 @@ func NewZeroconf(log librespot.Logger, port int, deviceName, deviceId string, de
return z, nil
}

func (z *Zeroconf) SetDeviceName(name string) {
z.deviceName = name

if err := z.registrar.UpdateName(name); err != nil {
z.log.WithError(err).Errorf("failed updating zeroconf service name to %q", name)
}
}

func (z *Zeroconf) SetCurrentUser(username string) {
z.userLock.Lock()
z.currentUser = username
Expand Down
Loading