package jellyfin
import (
"context"
"encoding/json"
"net"
"strconv"
"strings"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/core/publicurl"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request"
"github.com/navidrome/navidrome/utils/gg"
)
const (
discoveryPort = 7359
discoveryQuery = "who is jellyfinserver?"
)
type discoveryInfo struct {
Address string `json:"Address"`
Id string `json:"Id"`
Name string `json:"Name"`
EndpointAddress *string `json:"EndpointAddress"`
}
type Discovery struct {
ds model.DataStore
serverIDVal string
}
func NewDiscovery(ds model.DataStore) *Discovery {
return &Discovery{ds: ds}
}
func (d *Discovery) serverID(ctx context.Context) string {
return resolveServerID(ctx, d.ds, &d.serverIDVal)
}
func (d *Discovery) Serve(ctx context.Context) {
if !hasAdvertisableAddress() {
log.Warn(ctx, "Jellyfin API: auto-discovery is off, a unix socket server needs a BaseURL with a host to advertise")
return
}
conn, err := net.ListenPacket("udp4", net.JoinHostPort("0.0.0.0", strconv.Itoa(discoveryPort)))
if err != nil {
log.Warn(ctx, "Jellyfin API: auto-discovery is off, the UDP port is unavailable. Is another Jellyfin server running?", "port", discoveryPort, err)
return
}
log.Info(ctx, "Jellyfin API: listening for auto-discovery broadcasts", "port", discoveryPort)
d.ServeOn(ctx, conn)
}
func (d *Discovery) ServeOn(ctx context.Context, conn net.PacketConn) {
defer conn.Close()
stop := context.AfterFunc(ctx, func() { _ = conn.Close() })
defer stop()
buf := make([]byte, 1024)
for {
n, remote, err := conn.ReadFrom(buf)
if err != nil {
if ctx.Err() == nil {
log.Error(ctx, "Jellyfin API: auto-discovery listener stopped", err)
}
return
}
if !strings.Contains(strings.ToLower(string(buf[:n])), discoveryQuery) {
continue
}
info := discoveryInfo{Address: discoveryAddress(ctx, remote), Id: d.serverID(ctx), Name: serverName()}
res, _ := json.Marshal(info)
log.Debug(ctx, "Jellyfin API: answering auto-discovery request", "from", remote.String(), "address", info.Address)
if _, err := conn.WriteTo(res, remote); err != nil {
log.Debug(ctx, "Jellyfin API: could not answer auto-discovery request", "to", remote.String(), err)
}
}
}
func hasAdvertisableAddress() bool {
return conf.Server.BaseHost != "" || !strings.HasPrefix(conf.Server.Address, "unix:")
}
func discoveryAddress(ctx context.Context, remote net.Addr) string {
scheme := gg.If(conf.Server.TLSEnabled(), "https", "http")
host := net.JoinHostPort(localIPFor(remote), strconv.Itoa(conf.Server.Port))
return publicurl.AbsoluteURL(request.WithServerAddress(ctx, scheme, host), consts.URLPathJellyfinAPI, nil)
}
func localIPFor(remote net.Addr) string {
if ip := parseIP(conf.Server.Address); ip.IsValid() && !ip.IsUnspecified() {
return ip.String()
}
c, err := net.Dial("udp", remote.String())
if err != nil {
return conf.Server.Address
}
defer c.Close()
return c.LocalAddr().(*net.UDPAddr).IP.String()
}