stats

package
v0.3.3 Latest Latest
Warning

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

Go to latest
Published: Nov 4, 2026 License: GPL-3.0 Imports: 22 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 struct {
	ID   int64
	Name string
}

AccountRef labels an account id on a statistics page.

type BandwidthRow added in v0.3.0

type BandwidthRow struct {
	AccountID int64
	DeviceID  int64
	Timespan  int
	At        int64
	Lan       int
	Bytes     int64
}

BandwidthRow is one StatisticsBandwidth series point.

type Bucket

type Bucket struct {
	Label     string `json:"label"`
	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 Dashboard added in v0.3.0

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

Dashboard is this node's in-memory Plex Dashboard store: devices, 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 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 struct {
	ID               int64
	ClientIdentifier string
	Name             string
	Platform         string
	CreatedAt        int64
}

Device is one client the Dashboard can name.

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"`
	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 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 struct {
	AccountID    int64
	DeviceID     int64
	Timespan     int
	At           int64
	MetadataType int
	Count        int64
	Duration     int64 // sum of item lengths in seconds
}

MediaRow is one StatisticsMedia series point.

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
}

type Page added in v0.3.0

type Page struct {
	Rows      []BandwidthRow
	Media     []MediaRow
	Devices   []Device
	Accounts  []AccountRef
	TotalSize int
}

Page is one statistics answer: the series window and the Device/Account lookup for ids that window names.

type Play

type Play struct {
	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 struct {
	Timespans                     []int
	AccountID                     int64
	DeviceID                      int64
	HasAccount, HasDevice, HasLan bool
	Lan                           int
	AtMin, AtMax                  int64
	ExactAt                       int64
	HasExact, HasAtMin, HasAtMax  bool
	Offset                        int
	Limit                         int
	HasLimit                      bool
	Sort                          string
	Desc                          bool
}

Query is the parsed Dashboard filter. Timespans empty means "every kept rollup except per-second" for bandwidth/media (0..4).

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 writes play records still held from a database outage (bounded by ctx); a no-op on a standalone server.

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) SnapshotProviders

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

func (*Service) Summary

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

type Summary

type Summary struct {
	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