stats

package
v0.3.6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 9, 2026 License: GPL-3.0 Imports: 25 Imported by: 0

Documentation

Overview

Package stats records owner analytics from observed playback and provider data. It never reconstructs device or bandwidth information from watch counts.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type AccountRef added in v0.3.0

type AccountRef = frontend.StatisticsAccountRef

type BandwidthRow added in v0.3.0

type BandwidthRow = frontend.StatisticsBandwidthRow

type Bucket

type Bucket struct {
	Label string `json:"label"`
	// Key identifies the bucket's subject when it has one: a title's or
	// show's rating key, or a viewer's user id (for artwork and avatars).
	Key       string `json:"key,omitempty"`
	Plays     int64  `json:"plays"`
	WatchedMS int64  `json:"watchedMs"`
	Bytes     int64  `json:"bytes"`
}

type Cost

type Cost struct {
	Kind     string  `json:"kind"`
	ID       string  `json:"id"`
	Monthly  float64 `json:"monthly"`
	Currency string  `json:"currency"`
}

type DailyDelivery added in v0.3.4

type DailyDelivery struct {
	Label     string `json:"label"`
	Direct    int64  `json:"direct"`
	Transcode int64  `json:"transcode"`
	Bytes     int64  `json:"bytes"`
}

type DailyKind added in v0.3.4

type DailyKind struct {
	Label     string `json:"label"`
	MovieMS   int64  `json:"movieMs"`
	EpisodeMS int64  `json:"episodeMs"`
}

DailyKind is one day's watch time split into movies and TV episodes.

type Dashboard added in v0.3.0

type Dashboard struct {
	// contains filtered or unexported fields
}

Dashboard keeps this node's Plex Dashboard series in memory. Production device identities are backed by the durable registry; embedded instances without a registry retain process-local identities. Series include per-second bandwidth, and completed-play counts. Clustered nodes each keep their own rings; there is no request pin, so a client reads the node it connected to. Request paths only walk the requested buckets.

func NewDashboard added in v0.3.0

func NewDashboard(opts DashboardOptions) *Dashboard

NewDashboard creates the anonymous device (id 1) the way a PMS does at start.

func (*Dashboard) Bandwidth added in v0.3.0

func (d *Dashboard) Bandwidth(q Query) Page

Bandwidth returns the filtered, paged bandwidth series.

func (*Dashboard) Device added in v0.3.0

func (d *Dashboard) Device(id int64) (Device, bool)

Device returns one row by numeric id.

func (*Dashboard) Devices added in v0.3.0

func (d *Dashboard) Devices() []Device

Devices lists every device this process has seen, id order.

func (*Dashboard) Media added in v0.3.0

func (d *Dashboard) Media(q Query) Page

Media returns the filtered, paged completed-play series.

func (*Dashboard) RecordBytes added in v0.3.0

func (d *Dashboard) RecordBytes(accountID int64, accountName, clientID, deviceName, platform string, lan bool, n int64)

RecordBytes adds client-facing playback bytes to the per-second series.

func (*Dashboard) RecordPlay added in v0.3.0

func (d *Dashboard) RecordPlay(accountID int64, accountName, clientID string, metadataType int, durationSec int64)

RecordPlay counts one completed play into the media series. durationSec is the item's length in seconds (PMS StatisticsMedia duration), summed per bucket.

func (*Dashboard) SeeDevice added in v0.3.0

func (d *Dashboard) SeeDevice(clientID, name, platform string) Device

SeeDevice records or updates a client. An empty client identifier is device 1.

type DashboardOptions added in v0.3.0

type DashboardOptions struct {
	Now func() time.Time
	Loc *time.Location
}

DashboardOptions configure the clock (tests).

type DebridSample

type DebridSample struct {
	ID                                                                     string
	Requests, Failures, Cached, NotCached, Blocked, LinksRefreshed, Reused int64
	// Bytes is what the streaming engine fetched through this account.
	Bytes int64
}

DebridSample is one debrid account's cumulative counters on this node.

type DecisionCounts added in v0.3.3

type DecisionCounts struct {
	DirectPlay int `json:"directplay"`
	Remux      int `json:"copy"`
	Transcode  int `json:"transcode"`
}

type Device added in v0.3.0

type Device = frontend.StatisticsDevice

type DeviceCount added in v0.3.3

type DeviceCount struct {
	Name  string `json:"name"`
	Plays int    `json:"plays"`
}

type ExperienceCause added in v0.3.3

type ExperienceCause struct {
	Kind         string   `json:"kind"`
	Reason       string   `json:"reason,omitempty"`
	Count        int      `json:"count,omitempty"`
	RemotePct    float64  `json:"remotePct,omitempty"`
	SourceKbps   int      `json:"sourceKbps,omitempty"`
	Device       string   `json:"device,omitempty"`
	ReasonKind   string   `json:"reasonKind,omitempty"`
	ReasonDetail string   `json:"reasonDetail,omitempty"`
	Share        float64  `json:"share,omitempty"`
	Switches     int      `json:"switches,omitempty"`
	Plays        int      `json:"plays,omitempty"`
	MedianMS     int64    `json:"medianMs,omitempty"`
	PerHour      *float64 `json:"perHour,omitempty"`
}

ExperienceCause is the most likely cause of a viewer's experience, picked by simple rules; the console words it.

type ExperienceDay added in v0.3.3

type ExperienceDay struct {
	Day                 string   `json:"day"`
	Plays               int      `json:"plays"`
	StartMedianMS       *int64   `json:"startMedianMs"`
	BufferEventsPerHour *float64 `json:"bufferEventsPerHour"`
}

type ExperienceMetrics added in v0.3.3

type ExperienceMetrics struct {
	Plays               int              `json:"plays"`
	MeasuredPlays       int              `json:"measuredPlays"`
	StartMedianMS       *int64           `json:"startMedianMs"`
	StartP90MS          *int64           `json:"startP90Ms"`
	BufferEventsPerHour *float64         `json:"bufferEventsPerHour"`
	BufferMS            int64            `json:"bufferMs"`
	MeasuredWatchedMS   int64            `json:"measuredWatchedMs"`
	FailedStarts        int              `json:"failedStarts"`
	FailedReasons       []ReasonCount    `json:"failedReasons"`
	Decisions           DecisionCounts   `json:"decisions"`
	TopTranscodeReason  *TranscodeReason `json:"topTranscodeReason"`
	SourceKbps          *int             `json:"sourceKbps"`
	DeliveredKbps       *int             `json:"deliveredKbps"`
	UHDPct              *float64         `json:"uhdPct"`
	HDRPct              *float64         `json:"hdrPct"`
	Switches            int              `json:"switches"`
	DownSwitches        int              `json:"downSwitches"`
	Finished            int              `json:"finished"`
	Abandoned           int              `json:"abandoned"`
	RemotePct           *float64         `json:"remotePct"`
	WatchedMS           int64            `json:"watchedMs"`
	// TopDevice is the device with the most plays, as recorded.
	TopDevice *DeviceCount `json:"topDevice"`
}

ExperienceMetrics summarises one viewer's (or the household's) plays.

type ExperienceOverview added in v0.3.3

type ExperienceOverview struct {
	Days          int                   `json:"days"`
	Since         int64                 `json:"since"`
	MeasuredSince int64                 `json:"measuredSince"`
	Household     ExperienceMetrics     `json:"household"`
	Viewers       []ExperienceViewerRow `json:"viewers"`
}

type ExperiencePlay added in v0.3.3

type ExperiencePlay struct {
	ID              string `json:"id"`
	UserID          int64  `json:"userId"`
	Username        string `json:"-"`
	Title           string `json:"title"`
	Device          string `json:"device"`
	Platform        string `json:"platform"`
	Started         int64  `json:"started"`
	Ended           int64  `json:"-"`
	Measured        bool   `json:"measured"`
	StartMS         *int64 `json:"startMs"`
	BufferEvents    *int   `json:"bufferEvents"`
	BufferMS        *int64 `json:"bufferMs"`
	Failed          bool   `json:"failed"`
	Error           string `json:"error"`
	Decision        string `json:"decision"`
	ReasonKind      string `json:"reasonKind"`
	ReasonDetail    string `json:"reasonDetail"`
	QualityFallback string `json:"qualityFallback,omitempty"`
	Origin          string `json:"origin"`
	Release         string `json:"release"`
	Remote          bool   `json:"remote"`
	WatchedMS       int64  `json:"watchedMs"`
	DurationMS      int64  `json:"durationMs"`
	Switches        int    `json:"switches"`
	Completed       bool   `json:"-"`
	// contains filtered or unexported fields
}

ExperiencePlay is one recorded play with its experience.

type ExperienceViewer added in v0.3.3

type ExperienceViewer struct {
	UserID        int64             `json:"userId"`
	Username      string            `json:"username"`
	Days          int               `json:"days"`
	Since         int64             `json:"since"`
	MeasuredSince int64             `json:"measuredSince"`
	Metrics       ExperienceMetrics `json:"metrics"`
	Trend         []ExperienceDay   `json:"trend"`
	Plays         []ExperiencePlay  `json:"plays"`
	Cause         ExperienceCause   `json:"cause"`
}

type ExperienceViewerRow added in v0.3.3

type ExperienceViewerRow struct {
	UserID   int64             `json:"userId"`
	Username string            `json:"username"`
	Metrics  ExperienceMetrics `json:"metrics"`
}

type HeatCell added in v0.3.4

type HeatCell struct {
	Weekday   int   `json:"weekday"`
	Hour      int   `json:"hour"`
	Plays     int64 `json:"plays"`
	WatchedMS int64 `json:"watchedMs"`
}

type IndexerUsage

type IndexerUsage struct {
	Indexer     string `json:"indexer"`
	Searches    int64  `json:"searches"`
	Successes   int64  `json:"successes"`
	Grabs       int64  `json:"grabs"`
	GrabSuccess int64  `json:"grabSuccess"`
	Budgets     int64  `json:"budgets"`
	LatencyMS   int64  `json:"latencyMs"`
	AgeSeconds  int64  `json:"ageSeconds"`
	AgeSamples  int64  `json:"ageSamples"`
}

type MediaRow added in v0.3.0

type MediaRow = frontend.StatisticsMediaRow

type NewsletterSettings

type NewsletterSettings struct {
	Enabled     bool   `json:"enabled"`
	Weekday     int    `json:"weekday"`
	Hour        int    `json:"hour"`
	UserID      int64  `json:"userId"`
	NextDue     int64  `json:"nextDue"`
	LastAttempt int64  `json:"lastAttempt"`
	LastStatus  string `json:"lastStatus"`
}

type Options

type Options struct {
	Newsletter    func(context.Context, string) error
	StopSession   func(string) error
	SessionExists func(string) bool
	Playback      core.Playback
	Providers     func() []usenet.ProviderStats
	Indexers      func(context.Context) ([]newznab.Remaining, error)
	Logger        *slog.Logger
	Now           func() time.Time
	// NodeID identifies this node in a cluster. Plays are tagged with it and
	// New closes only this node's unfinished plays. Empty for a standalone
	// server, which therefore closes every unfinished play as before.
	NodeID string
	// Availability, when set (clustered nodes), holds play records the
	// database cannot take through an outage (the latest per play) and
	// skips periodic provider snapshots while the database is down; their
	// counters are cumulative, so nothing is lost. Nil writes synchronously.
	Availability *store.Availability
	// MetadataType maps a rating key to a Plex metadata type number for
	// Dashboard media rows. Zero or nil skips the play.
	MetadataType func(core.RatingKey) int
	// AccountName labels a user on Dashboard media rows.
	AccountName func(int64) string
	// Debrid and Stream report this node's cumulative debrid account and
	// streaming-engine counters; the sampler keeps their daily growth.
	Debrid func() []DebridSample
	Stream func() (StreamSample, bool)
}

type Page added in v0.3.0

type Page = frontend.StatisticsPage

type Play

type Play struct {
	IP         *string `json:"ip"`
	ID         string  `json:"id"`
	UserID     int64   `json:"userId"`
	Key        int64   `json:"ratingKey"`
	Username   string  `json:"username"`
	Title      string  `json:"title"`
	Platform   string  `json:"platform"`
	Device     string  `json:"device"`
	Decision   string  `json:"decision"`
	Started    int64   `json:"started"`
	Updated    int64   `json:"updated"`
	Ended      int64   `json:"ended"`
	Bytes      int64   `json:"bytes"`
	WatchedMS  int64   `json:"watchedMs"`
	DurationMS int64   `json:"durationMs"`
	Completed  bool    `json:"completed"`
	Failovers  int     `json:"failovers"`
	Error      string  `json:"error"`
}

type Query added in v0.3.0

type Query = frontend.StatisticsQuery

type ReasonCount added in v0.3.3

type ReasonCount struct {
	Reason string `json:"reason"`
	Count  int    `json:"count"`
}

type Service

type Service struct {
	// contains filtered or unexported fields
}

func New

func New(db *sql.DB, opts Options) *Service

func (*Service) Close

func (s *Service) Close(ctx context.Context) error

Close flushes device metadata and play records held through a database outage. The caller controls the shutdown budget with ctx.

func (*Service) Dashboard added in v0.3.0

func (s *Service) Dashboard() *Dashboard

Dashboard is the in-memory Plex Dashboard store for this node.

func (*Service) Experience added in v0.3.3

func (s *Service) Experience(ctx context.Context, days int) (ExperienceOverview, error)

Experience returns the household and per-viewer experience for the last days (7 or 30).

func (*Service) ExperienceForViewer added in v0.3.3

func (s *Service) ExperienceForViewer(ctx context.Context, user int64, days int) (ExperienceViewer, error)

ExperienceForViewer returns one viewer's metrics, daily trend, last 20 plays and the most likely cause of a poor experience.

func (*Service) ExperienceHandler added in v0.3.3

func (s *Service) ExperienceHandler() http.Handler

ExperienceHandler serves Activity › Experience; mount it behind owner authentication with /experience stripped.

func (*Service) Handler

func (s *Service) Handler() http.Handler

Handler must be mounted behind owner authentication with /stats stripped.

func (*Service) History

func (s *Service) History(ctx context.Context, since, until time.Time, user int64, limit int) ([]Play, error)

func (*Service) IndexerMonth

func (s *Service) IndexerMonth(ctx context.Context) ([]IndexerUsage, error)

func (*Service) MaybeNewsletter

func (s *Service) MaybeNewsletter(ctx context.Context) error

MaybeNewsletter claims each weekly slot in a durable ledger before enqueueing. Queue errors remain visible and are not retried automatically, preventing duplicates after an ambiguous process interruption.

func (*Service) NewsletterSettings

func (s *Service) NewsletterSettings(ctx context.Context) (NewsletterSettings, error)

func (*Service) ObserveIndexer

func (s *Service) ObserveIndexer(e newznab.Event)

func (*Service) ObservePlayback

func (s *Service) ObservePlayback(ev playback.Event)

ObservePlayback is suitable for playback.Manager.Subscribe.

func (*Service) ObserveState

func (s *Service) ObserveState(ev state.Event)

ObserveState correlates timeline data only when exactly one active session matches the user/title. Plex state events do not identify the client.

func (*Service) OutboxStatus

func (s *Service) OutboxStatus() (st store.OutboxStatus, ok bool)

OutboxStatus reports play records held through a database outage; ok is false on a standalone server.

func (*Service) PlayByID added in v0.3.3

func (s *Service) PlayByID(ctx context.Context, id string) (map[string]any, error)

PlayByID returns one recorded play with its experience fields merged into the History play's; nil when unknown.

func (*Service) Prune

func (s *Service) Prune(ctx context.Context) (int64, error)

Prune deletes per-day indexer/provider counters older than dailyRetentionDays. It is meant to be called periodically.

func (*Service) Record

func (s *Service) Record(ctx context.Context, p *core.PlaySession, ended bool, problem string) error

Record observes a playback session and upserts the history row of the viewing it belongs to (viewing.go): sessions of one viewer, client and title within the play-session window share a row, and a row is written only once media was delivered or the attempt failed. On a clustered node a record the database cannot take is held and written when it is back.

func (*Service) RecordIndexer

func (s *Service) RecordIndexer(ctx context.Context, e newznab.Event) error

func (*Service) RenderNewsletter

func (s *Service) RenderNewsletter(ctx context.Context, userID int64) (string, error)

func (*Service) Run

func (s *Service) Run(ctx context.Context)

Run samples active bandwidth and provider counters until cancellation.

func (*Service) Sample

func (s *Service) Sample(ctx context.Context)

func (*Service) SaveNewsletter

func (s *Service) SaveNewsletter(ctx context.Context, v NewsletterSettings) error

func (*Service) SetCost

func (s *Service) SetCost(ctx context.Context, c Cost) error

func (*Service) SnapshotDebrid

func (s *Service) SnapshotDebrid(ctx context.Context, ps []DebridSample) error

SnapshotDebrid adds each account's counter growth since the last sample to today's row. Unchanged accounts write nothing.

func (*Service) SnapshotProviders

func (s *Service) SnapshotProviders(ctx context.Context, ps []usenet.ProviderStats) error

func (*Service) SnapshotStream

func (s *Service) SnapshotStream(ctx context.Context, p StreamSample) error

SnapshotStream adds the engine's counter growth since the last sample to today's row.

func (*Service) SourceHistoryFor

func (s *Service) SourceHistoryFor(ctx context.Context, days int) (SourceHistory, error)

SourceHistoryFor reads the last `days` UTC days (today included). Every query is bounded by the window: daily tables by their (source, day) keys, plays by the started index.

func (*Service) Summary

func (s *Service) Summary(ctx context.Context, since, until time.Time, user int64, tzMinutes ...int) (Summary, error)

type SourceDebridDay

type SourceDebridDay struct {
	ID             string `json:"id"`
	Day            string `json:"day"`
	Requests       int64  `json:"requests"`
	Failures       int64  `json:"failures"`
	Cached         int64  `json:"cached"`
	NotCached      int64  `json:"notCached"`
	Blocked        int64  `json:"blocked"`
	LinksRefreshed int64  `json:"linksRefreshed"`
	Reused         int64  `json:"reused"`
	Bytes          int64  `json:"bytes"`
}

type SourceDebridTotal

type SourceDebridTotal struct {
	ID        string `json:"id"`
	FirstDay  string `json:"firstDay"`
	Requests  int64  `json:"requests"`
	Failures  int64  `json:"failures"`
	Cached    int64  `json:"cached"`
	NotCached int64  `json:"notCached"`
	Bytes     int64  `json:"bytes"`
}

type SourceHistory

type SourceHistory struct {
	Days         int                 `json:"days"`
	Since        string              `json:"since"`
	Until        string              `json:"until"`
	Plays        []SourcePlayDay     `json:"plays"`
	ProviderDays []SourceProviderDay `json:"providerDays"`
	IndexerDays  []SourceIndexerDay  `json:"indexerDays"`
	DebridDays   []SourceDebridDay   `json:"debridDays"`
	StreamDays   []SourceStreamDay   `json:"streamDays"`
	// Hours splits today's plays by UTC hour; only for a one-day window.
	Hours []SourcePlayHour `json:"hours"`
	// Lifetime totals each source over every retained day (daily rows are
	// kept two years; see dailyRetentionDays).
	Lifetime SourceLifetime `json:"lifetime"`
}

SourceHistory is the Statistics document: daily rows per source over a bounded window of UTC days (since..until inclusive).

type SourceIndexerDay

type SourceIndexerDay struct {
	Indexer     string `json:"indexer"`
	Day         string `json:"day"`
	Searches    int64  `json:"searches"`
	Successes   int64  `json:"successes"`
	Grabs       int64  `json:"grabs"`
	GrabSuccess int64  `json:"grabSuccess"`
	Budgets     int64  `json:"budgets"`
	LatencyMS   int64  `json:"latencyMs"`
	AgeSeconds  int64  `json:"ageSeconds"`
	AgeSamples  int64  `json:"ageSamples"`
}

type SourceIndexerTotal

type SourceIndexerTotal struct {
	Indexer     string `json:"indexer"`
	FirstDay    string `json:"firstDay"`
	Searches    int64  `json:"searches"`
	Successes   int64  `json:"successes"`
	Grabs       int64  `json:"grabs"`
	GrabSuccess int64  `json:"grabSuccess"`
}

type SourceLifetime

type SourceLifetime struct {
	Providers []SourceProviderTotal `json:"providers"`
	Indexers  []SourceIndexerTotal  `json:"indexers"`
	Debrid    []SourceDebridTotal   `json:"debrid"`
}

SourceLifetime is every source's totals since its first recorded day.

type SourcePlayDay

type SourcePlayDay struct {
	Day       string `json:"day"`
	Origin    string `json:"origin"`
	Plays     int64  `json:"plays"`
	Failovers int64  `json:"failovers"`
	Failed    int64  `json:"failed"`
}

SourcePlayDay counts plays started on a UTC day by where they streamed from.

type SourcePlayHour

type SourcePlayHour struct {
	Hour      int    `json:"hour"`
	Origin    string `json:"origin"`
	Plays     int64  `json:"plays"`
	Failovers int64  `json:"failovers"`
	Failed    int64  `json:"failed"`
}

SourcePlayHour counts today's plays started in one UTC hour (0..23).

type SourceProviderDay

type SourceProviderDay struct {
	ID            string `json:"id"`
	Day           string `json:"day"`
	Bytes         int64  `json:"bytes"`
	Articles      int64  `json:"articles"`
	Misses        int64  `json:"misses"`
	Errors        int64  `json:"errors"`
	BackfillSaves int64  `json:"backfillSaves"`
	BodyMisses    int64  `json:"bodyMisses"`
	BodyTakedowns int64  `json:"bodyTakedowns"`
	Spillovers    int64  `json:"spillovers"`
}

type SourceProviderTotal

type SourceProviderTotal struct {
	ID       string `json:"id"`
	FirstDay string `json:"firstDay"`
	Bytes    int64  `json:"bytes"`
	Articles int64  `json:"articles"`
	Misses   int64  `json:"misses"`
	Errors   int64  `json:"errors"`
}

type SourceStreamDay

type SourceStreamDay struct {
	Day           string `json:"day"`
	MemoryHits    int64  `json:"memoryHits"`
	DiskHits      int64  `json:"diskHits"`
	BlocksFetched int64  `json:"blocksFetched"`
	BytesFetched  int64  `json:"bytesFetched"`
	Hedges        int64  `json:"hedges"`
	HedgeWins     int64  `json:"hedgeWins"`
	LinkRefreshes int64  `json:"linkRefreshes"`
	Errors        int64  `json:"errors"`
}

type StreamSample

type StreamSample struct {
	MemoryHits, DiskHits, BlocksFetched, BytesFetched, Hedges, HedgeWins, LinkRefreshes, Errors int64
}

StreamSample is the debrid streaming engine's cumulative counters on this node: block cache hits against blocks fetched from the CDN.

type Summary

type Summary struct {
	Heatmap        []HeatCell      `json:"heatmap"`
	DailyKinds     []DailyKind     `json:"dailyKinds"`
	DailyDelivery  []DailyDelivery `json:"dailyDelivery"`
	PeakStreams    int             `json:"peakStreams"`
	AvgBitrateKbps float64         `json:"avgBitrateKbps"`
	Decisions      []Bucket        `json:"decisions"`
	Origins        []Bucket        `json:"origins"`
	Qualities      []Bucket        `json:"qualities"`
	Places         []Bucket        `json:"places"`
	Shows          []Bucket        `json:"shows"`
	Genres         []Bucket        `json:"genres"`
	Kinds          []Bucket        `json:"kinds"`
	Plays          int64           `json:"plays"`
	WatchedMS      int64           `json:"watchedMs"`
	Bytes          int64           `json:"bytes"`
	Completed      int64           `json:"completed"`
	Daily          []Bucket        `json:"daily"`
	Hourly         []Bucket        `json:"hourly"`
	Platforms      []Bucket        `json:"platforms"`
	Titles         []Bucket        `json:"titles"`
	Users          []Bucket        `json:"users"`
	Since          time.Time       `json:"since"`
	Until          time.Time       `json:"until"`
}

type TranscodeReason added in v0.3.3

type TranscodeReason struct {
	Kind   string `json:"kind"`
	Detail string `json:"detail"`
	Count  int    `json:"count"`
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL