Documentation
¶
Overview ¶
Package stats records owner analytics from observed playback and provider data. It never reconstructs device or bandwidth information from watch counts.
Index ¶
- type AccountRef
- type BandwidthRow
- type Bucket
- type Cost
- type DailyDelivery
- type DailyKind
- type Dashboard
- func (d *Dashboard) Bandwidth(q Query) Page
- func (d *Dashboard) Device(id int64) (Device, bool)
- func (d *Dashboard) Devices() []Device
- func (d *Dashboard) Media(q Query) Page
- func (d *Dashboard) RecordBytes(accountID int64, accountName, clientID, deviceName, platform string, lan bool, ...)
- func (d *Dashboard) RecordPlay(accountID int64, accountName, clientID string, metadataType int, ...)
- func (d *Dashboard) SeeDevice(clientID, name, platform string) Device
- type DashboardOptions
- type DebridSample
- type DecisionCounts
- type Device
- type DeviceCount
- type ExperienceCause
- type ExperienceDay
- type ExperienceMetrics
- type ExperienceOverview
- type ExperiencePlay
- type ExperienceViewer
- type ExperienceViewerRow
- type HeatCell
- type IndexerUsage
- type MediaRow
- type NewsletterSettings
- type Options
- type Page
- type Play
- type Query
- type ReasonCount
- type Service
- func (s *Service) Close(ctx context.Context) error
- func (s *Service) Dashboard() *Dashboard
- func (s *Service) Experience(ctx context.Context, days int) (ExperienceOverview, error)
- func (s *Service) ExperienceForViewer(ctx context.Context, user int64, days int) (ExperienceViewer, error)
- func (s *Service) ExperienceHandler() http.Handler
- func (s *Service) Handler() http.Handler
- func (s *Service) History(ctx context.Context, since, until time.Time, user int64, limit int) ([]Play, error)
- func (s *Service) IndexerMonth(ctx context.Context) ([]IndexerUsage, error)
- func (s *Service) MaybeNewsletter(ctx context.Context) error
- func (s *Service) NewsletterSettings(ctx context.Context) (NewsletterSettings, error)
- func (s *Service) ObserveIndexer(e newznab.Event)
- func (s *Service) ObservePlayback(ev playback.Event)
- func (s *Service) ObserveState(ev state.Event)
- func (s *Service) OutboxStatus() (st store.OutboxStatus, ok bool)
- func (s *Service) PlayByID(ctx context.Context, id string) (map[string]any, error)
- func (s *Service) Prune(ctx context.Context) (int64, error)
- func (s *Service) Record(ctx context.Context, p *core.PlaySession, ended bool, problem string) error
- func (s *Service) RecordIndexer(ctx context.Context, e newznab.Event) error
- func (s *Service) RenderNewsletter(ctx context.Context, userID int64) (string, error)
- func (s *Service) Run(ctx context.Context)
- func (s *Service) Sample(ctx context.Context)
- func (s *Service) SaveNewsletter(ctx context.Context, v NewsletterSettings) error
- func (s *Service) SetCost(ctx context.Context, c Cost) error
- func (s *Service) SnapshotDebrid(ctx context.Context, ps []DebridSample) error
- func (s *Service) SnapshotProviders(ctx context.Context, ps []usenet.ProviderStats) error
- func (s *Service) SnapshotStream(ctx context.Context, p StreamSample) error
- func (s *Service) SourceHistoryFor(ctx context.Context, days int) (SourceHistory, error)
- func (s *Service) Summary(ctx context.Context, since, until time.Time, user int64, tzMinutes ...int) (Summary, error)
- type SourceDebridDay
- type SourceDebridTotal
- type SourceHistory
- type SourceIndexerDay
- type SourceIndexerTotal
- type SourceLifetime
- type SourcePlayDay
- type SourcePlayHour
- type SourceProviderDay
- type SourceProviderTotal
- type SourceStreamDay
- type StreamSample
- type Summary
- type TranscodeReason
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 DailyDelivery ¶ added in v0.3.4
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
Bandwidth returns the filtered, paged bandwidth series.
func (*Dashboard) Devices ¶ added in v0.3.0
Devices lists every device this process has seen, id order.
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.
type DashboardOptions ¶ added in v0.3.0
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 Device ¶ added in v0.3.0
type Device = frontend.StatisticsDevice
type DeviceCount ¶ added in v0.3.3
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"`
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 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 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 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 Service ¶
type Service struct {
// contains filtered or unexported fields
}
func (*Service) Close ¶
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
Dashboard is the in-memory Plex Dashboard store for this node.
func (*Service) Experience ¶ added in v0.3.3
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
ExperienceHandler serves Activity › Experience; mount it behind owner authentication with /experience stripped.
func (*Service) IndexerMonth ¶
func (s *Service) IndexerMonth(ctx context.Context) ([]IndexerUsage, error)
func (*Service) MaybeNewsletter ¶
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 (*Service) ObservePlayback ¶
ObservePlayback is suitable for playback.Manager.Subscribe.
func (*Service) ObserveState ¶
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
PlayByID returns one recorded play with its experience fields merged into the History play's; nil when unknown.
func (*Service) Prune ¶
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 (*Service) RenderNewsletter ¶
func (*Service) SaveNewsletter ¶
func (s *Service) SaveNewsletter(ctx context.Context, v NewsletterSettings) 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 (*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 ¶
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.
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 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 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 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"`
}