diff --git a/.dockerignore b/.dockerignore index b53d7842a..596aa2955 100644 --- a/.dockerignore +++ b/.dockerignore @@ -14,4 +14,5 @@ tmp dist binaries cache -music \ No newline at end of file +music +!Dockerfile \ No newline at end of file diff --git a/Makefile b/Makefile index 87cadc71e..12e2039b9 100644 --- a/Makefile +++ b/Makefile @@ -25,11 +25,11 @@ setup: check_env download-deps setup-git ##@1_Run_First Install dependencies and .PHONY: setup dev: check_env ##@Development Start Navidrome in development mode, with hot-reload for both frontend and backend - npx foreman -j Procfile.dev -p 4533 start + ND_ENABLEINSIGHTSCOLLECTOR="false" npx foreman -j Procfile.dev -p 4533 start .PHONY: dev server: check_go_env buildjs ##@Development Start the backend in development mode - @go run github.com/cespare/reflex@latest -d none -c reflex.conf + @ND_ENABLEINSIGHTSCOLLECTOR="false" go run github.com/cespare/reflex@latest -d none -c reflex.conf .PHONY: server watch: ##@Development Start Go tests in watch mode (re-run when code changes) diff --git a/cmd/root.go b/cmd/root.go index d13a092ef..7a3104543 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -208,6 +208,7 @@ func startInsightsCollector(ctx context.Context) func() error { return nil } log.Info(ctx, "Starting Insight Collector") + time.Sleep(conf.Server.DevInsightsInitialDelay) ic := CreateInsights() ic.Run(ctx) return nil diff --git a/conf/configuration.go b/conf/configuration.go index 17e8caedf..8d5794c66 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -113,6 +113,8 @@ type configOptions struct { DevArtworkThrottleBacklogTimeout time.Duration DevArtistInfoTimeToLive time.Duration DevAlbumInfoTimeToLive time.Duration + DevInsightsInitialDelay time.Duration + DevEnablePlayerInsights bool } type scannerOptions struct { @@ -470,6 +472,8 @@ func init() { viper.SetDefault("devartworkthrottlebacklogtimeout", consts.RequestThrottleBacklogTimeout) viper.SetDefault("devartistinfotimetolive", consts.ArtistInfoTimeToLive) viper.SetDefault("devalbuminfotimetolive", consts.AlbumInfoTimeToLive) + viper.SetDefault("devinsightsinitialdelay", consts.InsightsInitialDelay) + viper.SetDefault("devenableplayerinsights", true) } func InitConfig(cfgFile string) { diff --git a/consts/consts.go b/consts/consts.go index f30964d43..fa57562a1 100644 --- a/consts/consts.go +++ b/consts/consts.go @@ -90,6 +90,7 @@ const ( InsightsIDKey = "InsightsID" InsightsEndpoint = "https://insights.navidrome.org/collect" InsightsUpdateInterval = 24 * time.Hour + InsightsInitialDelay = 30 * time.Minute ) var ( diff --git a/core/metrics/insights.go b/core/metrics/insights.go index 3ac65c1fe..97e088671 100644 --- a/core/metrics/insights.go +++ b/core/metrics/insights.go @@ -16,6 +16,7 @@ import ( "github.com/google/uuid" "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/consts" + "github.com/navidrome/navidrome/core/auth" "github.com/navidrome/navidrome/core/metrics/insights" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" @@ -54,6 +55,7 @@ func GetInstance(ds model.DataStore) Insights { } func (c *insightsCollector) Run(ctx context.Context) { + ctx = auth.WithAdminUser(ctx, c.ds) for { c.sendInsights(ctx) select { @@ -199,15 +201,45 @@ func (c *insightsCollector) collect(ctx context.Context) []byte { data.Uptime = time.Since(consts.ServerStart).Milliseconds() / 1000 // Library info - data.Library.Tracks, _ = c.ds.MediaFile(ctx).CountAll() - data.Library.Albums, _ = c.ds.Album(ctx).CountAll() - data.Library.Artists, _ = c.ds.Artist(ctx).CountAll() - data.Library.Playlists, _ = c.ds.Playlist(ctx).Count() - data.Library.Shares, _ = c.ds.Share(ctx).CountAll() - data.Library.Radios, _ = c.ds.Radio(ctx).Count() - data.Library.ActiveUsers, _ = c.ds.User(ctx).CountAll(model.QueryOptions{ + var err error + data.Library.Tracks, err = c.ds.MediaFile(ctx).CountAll() + if err != nil { + log.Trace(ctx, "Error reading tracks count", err) + } + data.Library.Albums, err = c.ds.Album(ctx).CountAll() + if err != nil { + log.Trace(ctx, "Error reading albums count", err) + } + data.Library.Artists, err = c.ds.Artist(ctx).CountAll() + if err != nil { + log.Trace(ctx, "Error reading artists count", err) + } + data.Library.Playlists, err = c.ds.Playlist(ctx).CountAll() + if err != nil { + log.Trace(ctx, "Error reading playlists count", err) + } + data.Library.Shares, err = c.ds.Share(ctx).CountAll() + if err != nil { + log.Trace(ctx, "Error reading shares count", err) + } + data.Library.Radios, err = c.ds.Radio(ctx).Count() + if err != nil { + log.Trace(ctx, "Error reading radios count", err) + } + data.Library.ActiveUsers, err = c.ds.User(ctx).CountAll(model.QueryOptions{ Filters: squirrel.Gt{"last_access_at": time.Now().Add(-7 * 24 * time.Hour)}, }) + if err != nil { + log.Trace(ctx, "Error reading active users count", err) + } + if conf.Server.DevEnablePlayerInsights { + data.Library.ActivePlayers, err = c.ds.Player(ctx).CountByClient(model.QueryOptions{ + Filters: squirrel.Gt{"last_seen": time.Now().Add(-7 * 24 * time.Hour)}, + }) + if err != nil { + log.Trace(ctx, "Error reading active players count", err) + } + } // Memory info var m runtime.MemStats diff --git a/core/metrics/insights/data.go b/core/metrics/insights/data.go index 9e0d1ade9..576bbd316 100644 --- a/core/metrics/insights/data.go +++ b/core/metrics/insights/data.go @@ -29,13 +29,14 @@ type Data struct { Backup *FSInfo `json:"backup,omitempty"` } `json:"fs"` Library struct { - Tracks int64 `json:"tracks"` - Albums int64 `json:"albums"` - Artists int64 `json:"artists"` - Playlists int64 `json:"playlists"` - Shares int64 `json:"shares"` - Radios int64 `json:"radios"` - ActiveUsers int64 `json:"activeUsers"` + Tracks int64 `json:"tracks"` + Albums int64 `json:"albums"` + Artists int64 `json:"artists"` + Playlists int64 `json:"playlists"` + Shares int64 `json:"shares"` + Radios int64 `json:"radios"` + ActiveUsers int64 `json:"activeUsers"` + ActivePlayers map[string]int64 `json:"activePlayers"` } `json:"library"` Config struct { LogLevel string `json:"logLevel,omitempty"` diff --git a/core/metrics/insights_linux.go b/core/metrics/insights_linux.go index 1e732f992..89769877b 100644 --- a/core/metrics/insights_linux.go +++ b/core/metrics/insights_linux.go @@ -41,22 +41,29 @@ type MountInfo struct { } var fsTypeMap = map[int64]string{ - // Add filesystem type mappings + 0x5346414f: "afs", + 0x61756673: "aufs", 0x9123683E: "btrfs", - 0x0000EF53: "ext2/ext3/ext4", - 0x00006969: "nfs", - 0x58465342: "xfs", - 0x2FC12FC1: "zfs", - 0x01021994: "tmpfs", 0x28cd3d45: "cramfs", 0x64626720: "debugfs", + 0x0000EF53: "ext2/ext3/ext4", + 0x6a656a63: "fakeowner", // FS inside a container + 0x65735546: "fuse", + 0x4244: "hfs", + 0x9660: "iso9660", + 0x3153464a: "jfs", + 0x00006969: "nfs", + 0x794c7630: "overlayfs", + 0x9fa0: "proc", + 0x517b: "smb", + 0xfe534d42: "smb2", 0x73717368: "squashfs", 0x62656572: "sysfs", - 0x9fa0: "proc", - 0x61756673: "aufs", - 0x794c7630: "overlayfs", - 0x6a656a63: "fakeowner", // FS inside a container - // Include other filesystem types as needed + 0x01021994: "tmpfs", + 0x01021997: "v9fs", + 0x4d44: "vfat", + 0x58465342: "xfs", + 0x2FC12FC1: "zfs", } func getFilesystemType(path string) (string, error) { diff --git a/model/player.go b/model/player.go index f0018cc82..ee7346b66 100644 --- a/model/player.go +++ b/model/player.go @@ -26,5 +26,7 @@ type PlayerRepository interface { Get(id string) (*Player, error) FindMatch(userId, client, userAgent string) (*Player, error) Put(p *Player) error + CountAll(...QueryOptions) (int64, error) + CountByClient(...QueryOptions) (map[string]int64, error) // TODO: Add CountAll method. Useful at least for metrics. } diff --git a/persistence/player_repository.go b/persistence/player_repository.go index d3f13e772..73c820753 100644 --- a/persistence/player_repository.go +++ b/persistence/player_repository.go @@ -74,8 +74,33 @@ func (r *playerRepository) addRestriction(sql ...Sqlizer) Sqlizer { return append(s, Eq{"user_id": u.ID}) } +func (r *playerRepository) CountByClient(options ...model.QueryOptions) (map[string]int64, error) { + sel := r.newSelect(options...). + Columns( + "case when client = 'NavidromeUI' then name else client end as player", + "count(*) as count", + ).GroupBy("client") + var res []struct { + Player string + Count int64 + } + err := r.queryAll(sel, &res) + if err != nil { + return nil, err + } + counts := make(map[string]int64, len(res)) + for _, c := range res { + counts[c.Player] = c.Count + } + return counts, nil +} + +func (r *playerRepository) CountAll(options ...model.QueryOptions) (int64, error) { + return r.count(r.newRestSelect(), options...) +} + func (r *playerRepository) Count(options ...rest.QueryOptions) (int64, error) { - return r.count(r.newRestSelect(), r.parseRestOptions(r.ctx, options...)) + return r.CountAll(r.parseRestOptions(r.ctx, options...)) } func (r *playerRepository) Read(id string) (interface{}, error) {