package jellyfin
import (
"context"
"encoding/hex"
"errors"
"fmt"
"net"
"net/http"
"net/netip"
"path"
"strings"
"sync"
"github.com/google/uuid"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/server"
"github.com/navidrome/navidrome/server/jellyfin/dto"
)
const jellyfinVersion = "12.1.0"
func serverName() string {
if conf.Server.Jellyfin.ServerName != "" {
return conf.Server.Jellyfin.ServerName
}
return fmt.Sprintf("Navidrome %s", consts.Version)
}
func (api *Router) serverID(ctx context.Context) string {
return resolveServerID(ctx, api.ds, &api.serverIDVal)
}
var serverIDMu sync.Mutex
func resolveServerID(ctx context.Context, ds model.DataStore, cached *string) string {
serverIDMu.Lock()
defer serverIDMu.Unlock()
if *cached != "" {
return *cached
}
if ds == nil {
*cached = newServerID()
return *cached
}
id, err := ds.Property().Get(ctx, consts.JellyfinServerIDKey)
switch {
case errors.Is(err, model.ErrNotFound):
id = newServerID()
if err := ds.Property().Put(ctx, consts.JellyfinServerIDKey, id); err != nil {
log.Error(ctx, "Jellyfin API: could not persist server id", err)
return id
}
case err != nil:
log.Error(ctx, "Jellyfin API: could not read server id", err)
return newServerID()
}
*cached = strings.ReplaceAll(id, "-", "")
return *cached
}
func newServerID() string {
u := uuid.New()
return hex.EncodeToString(u[:])
}
func (api *Router) publicInfo(r *http.Request) dto.PublicSystemInfo {
return dto.PublicSystemInfo{
LocalAddress: localAddress(r),
ServerName: serverName(),
Version: jellyfinVersion,
ProductName: "Jellyfin Server",
Id: api.serverID(r.Context()),
StartupWizardCompleted: true,
}
}
func localAddress(r *http.Request) string {
scheme, host := server.ServerAddress(r)
return scheme + "://" + host + path.Join(conf.Server.BasePath, consts.URLPathJellyfinAPI)
}
func (api *Router) getPublicSystemInfo(w http.ResponseWriter, r *http.Request) {
api.ok(w, r, api.publicInfo(r))
}
func (api *Router) getSystemInfo(w http.ResponseWriter, r *http.Request) {
api.ok(w, r, dto.SystemInfo{
PublicSystemInfo: api.publicInfo(r),
SupportsLibraryMonitor: true,
})
}
func (api *Router) ping(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(serverName()))
}
func (api *Router) getEndpointInfo(w http.ResponseWriter, r *http.Request) {
remote := remoteIP(r)
api.ok(w, r, dto.EndPointInfo{
IsLocal: isSameMachine(r, remote),
IsInNetwork: isInLocalNetwork(remote),
})
}
func isInLocalNetwork(ip netip.Addr) bool {
return ip.IsLoopback() || ip.IsPrivate() || (ip.Is6() && ip.IsLinkLocalUnicast())
}
func isSameMachine(r *http.Request, remote netip.Addr) bool {
local, ok := r.Context().Value(http.LocalAddrContextKey).(net.Addr)
if !ok {
return remote.IsLoopback()
}
return parseIP(local.String()) == remote
}
func remoteIP(r *http.Request) netip.Addr {
return parseIP(r.RemoteAddr)
}
func parseIP(addr string) netip.Addr {
if h, _, err := net.SplitHostPort(addr); err == nil {
addr = h
}
ip, err := netip.ParseAddr(addr)
if err != nil {
return netip.Addr{}
}
return ip.Unmap()
}