Documentation
¶
Overview ¶
Package state implements per-user watch state and playlists over SQLite: core.State and core.Playlists.
Semantics follow Plex:
- Progress at >= 90% of duration marks the item watched (view count increments once per viewing, the resume offset clears).
- Progress below 5% is treated as "not really started": no resume point is kept. Between 5% and 90% the item is in progress.
- Scrobble marks watched unconditionally; Unscrobble clears the view count and resume point (the user's rating is kept).
Index ¶
- Constants
- func Fraction(ws core.WatchStatus) float64
- func ImportPlaylistTx(ctx context.Context, tx *sql.Tx, id, userID int64, title string, ...) (int64, error)
- func IsInProgress(ws core.WatchStatus) bool
- func IsWatched(ws core.WatchStatus) bool
- type Event
- type EventKind
- type ExportEntry
- type ImportBatch
- type ImportRecord
- type Options
- type Rollup
- type State
- func (s *State) Add(ctx context.Context, userID int64, key core.RatingKey, items []core.RatingKey) error
- func (s *State) ChildrenStatus(ctx context.Context, userID int64, children []core.RatingKey) (Rollup, map[core.RatingKey]core.WatchStatus, error)
- func (s *State) Close(ctx context.Context) error
- func (s *State) Create(ctx context.Context, userID int64, title string, items []core.RatingKey) (*core.Playlist, error)
- func (s *State) Delete(ctx context.Context, userID int64, key core.RatingKey) error
- func (s *State) ExportEntries(ctx context.Context, userID int64, ratings bool) ([]ExportEntry, error)
- func (s *State) Get(ctx context.Context, userID int64, key core.RatingKey) (core.WatchStatus, error)
- func (s *State) GetMany(ctx context.Context, userID int64, keys []core.RatingKey) (map[core.RatingKey]core.WatchStatus, error)
- func (s *State) GetPlaylist(ctx context.Context, userID int64, key core.RatingKey) (*core.Playlist, error)
- func (s *State) History(ctx context.Context, userID int64, since time.Time, limit int) ([]core.HistoryEntry, error)
- func (s *State) ImportTransaction(ctx context.Context, prepare func(*sql.Tx) (ImportBatch, error)) error
- func (s *State) InProgress(ctx context.Context, userID int64, limit int) ([]core.WatchStatus, error)
- func (s *State) InProgressAny(ctx context.Context) ([]core.RatingKey, error)
- func (s *State) List(ctx context.Context, userID int64) ([]core.Playlist, error)
- func (s *State) Move(ctx context.Context, userID int64, key core.RatingKey, from, to int) error
- func (s *State) OutboxStatus() (st store.OutboxStatus, ok bool)
- func (s *State) Playlists() core.Playlists
- func (s *State) Progress(ctx context.Context, userID int64, key core.RatingKey, ...) error
- func (s *State) Rate(ctx context.Context, userID int64, key core.RatingKey, rating float64) error
- func (s *State) Remove(ctx context.Context, userID int64, key core.RatingKey, itemIndex int) error
- func (s *State) Scrobble(ctx context.Context, userID int64, key core.RatingKey) error
- func (s *State) ScrobbleMany(ctx context.Context, userID int64, keys []core.RatingKey) error
- func (s *State) Subscribe(fn func(Event)) func()
- func (s *State) Unscrobble(ctx context.Context, userID int64, key core.RatingKey) error
- func (s *State) UnscrobbleMany(ctx context.Context, userID int64, keys []core.RatingKey) error
- func (s *State) WatchedEpisodes(ctx context.Context, userID int64) ([]core.RatingKey, error)
Constants ¶
const ( WatchedThreshold = 0.90 StartedThreshold = 0.05 )
WatchedThreshold and StartedThreshold are the fractions of duration at which an item counts as watched / meaningfully started.
Variables ¶
This section is empty.
Functions ¶
func Fraction ¶
func Fraction(ws core.WatchStatus) float64
Fraction returns offset/duration, or 0 when the duration is unknown.
func ImportPlaylistTx ¶
func ImportPlaylistTx(ctx context.Context, tx *sql.Tx, id, userID int64, title string, keys []core.RatingKey, now int64) (int64, error)
ImportPlaylistTx preserves imported playlist IDs while enforcing ownership.
func IsInProgress ¶
func IsInProgress(ws core.WatchStatus) bool
IsInProgress reports whether a status has a resume point.
func IsWatched ¶
func IsWatched(ws core.WatchStatus) bool
IsWatched reports whether a status counts as watched (watched at least once).
Types ¶
type Event ¶
type Event struct {
Kind EventKind
UserID int64
Key core.RatingKey
Offset time.Duration
Duration time.Duration
Fraction float64
Rating float64 // EventRated only
State string // raw timeline state for EventProgress: playing, paused, stopped
Completed bool
At time.Time
Imported bool // reviewed history import; never an active playback timeline
}
Event is a watch-state change. Fraction is offset/duration (0 when the duration is unknown). Completed is true for EventCompleted/EventScrobble.
type EventKind ¶
type EventKind string
EventKind identifies a state change subscribers may react to.
const ( EventProgress EventKind = "progress" // a timeline update (incl. start/stop) EventCompleted EventKind = "completed" // crossed the watched threshold EventScrobble EventKind = "scrobble" // explicitly marked watched EventUnscrobble EventKind = "unscrobble" // explicitly marked unwatched EventRated EventKind = "rated" )
type ExportEntry ¶
ExportEntry is the state-owned, source-neutral view used by CSV exports.
type ImportBatch ¶
type ImportBatch struct{ Records, History []ImportRecord }
type ImportRecord ¶
type ImportRecord struct {
UserID int64
Key core.RatingKey
Views int
Offset, Duration time.Duration
Rating float64
ViewedAt time.Time
RatingsOnly bool
}
ImportRecord uses native time units; source-specific parsing belongs to the importer. RatingsOnly preserves existing progress and completion counts.
type Options ¶
type Options struct {
Logger *slog.Logger
Now func() time.Time
// Availability, when set (a clustered node on a shared PostgreSQL
// database), makes timeline progress survive a database outage: a
// Progress the database cannot take is held in memory (the latest per
// user and title) and written when the database answers again, so
// players are never blocked or failed by the outage. Nil (standalone)
// writes progress synchronously as before.
Availability *store.Availability
}
Options tune a State. Now is injectable for tests.
type Rollup ¶
type Rollup struct {
Total int
Watched int // viewCount > 0
InProgress int // has a resume point
AllWatched bool // Total > 0 && Watched == Total
AnyWatched bool
}
Rollup summarises the watched state of a set of children (the episodes of a season/show) for Plex's viewedLeafCount-style fields.
type State ¶
type State struct {
// contains filtered or unexported fields
}
State implements core.State and core.Playlists.
func (*State) Add ¶
func (s *State) Add(ctx context.Context, userID int64, key core.RatingKey, items []core.RatingKey) error
Add appends items to a playlist.
func (*State) ChildrenStatus ¶
func (s *State) ChildrenStatus(ctx context.Context, userID int64, children []core.RatingKey) (Rollup, map[core.RatingKey]core.WatchStatus, error)
ChildrenStatus fetches statuses for children and rolls them up.
func (*State) Close ¶
Close writes progress still held from a database outage (bounded by ctx). It is a no-op on a standalone server.
func (*State) Create ¶
func (s *State) Create(ctx context.Context, userID int64, title string, items []core.RatingKey) (*core.Playlist, error)
Create makes a playlist with the given items in order.
func (*State) ExportEntries ¶
func (*State) Get ¶
func (s *State) Get(ctx context.Context, userID int64, key core.RatingKey) (core.WatchStatus, error)
Get returns the status for one key. A never-touched key returns a zero status (with Key set) and no error.
func (*State) GetMany ¶
func (s *State) GetMany(ctx context.Context, userID int64, keys []core.RatingKey) (map[core.RatingKey]core.WatchStatus, error)
GetMany returns a status for every requested key (zero when untouched).
func (*State) GetPlaylist ¶
func (s *State) GetPlaylist(ctx context.Context, userID int64, key core.RatingKey) (*core.Playlist, error)
Get returns one playlist owned by the user, or core.ErrNotFound.
func (*State) History ¶
func (s *State) History(ctx context.Context, userID int64, since time.Time, limit int) ([]core.HistoryEntry, error)
History returns the viewing log, newest first. A zero since means all.
func (*State) ImportTransaction ¶
func (s *State) ImportTransaction(ctx context.Context, prepare func(*sql.Tx) (ImportBatch, error)) error
ImportTransaction commits a reviewed import with the caller's own bookkeeping and list writes. State owns every watch/history write and emits ordinary state events only after the complete transaction succeeds. Repeated history is inert.
func (*State) InProgress ¶
func (s *State) InProgress(ctx context.Context, userID int64, limit int) ([]core.WatchStatus, error)
InProgress returns items with a resume point, most recently viewed first.
func (*State) InProgressAny ¶
InProgressAny lists distinct keys in progress for any user.
func (*State) Move ¶
Move moves the item at from so it lands at index to (both 0-based, to is the final position in the resulting list).
func (*State) OutboxStatus ¶
func (s *State) OutboxStatus() (st store.OutboxStatus, ok bool)
OutboxStatus reports timeline writes held through a database outage; ok is false when progress is written synchronously (standalone).
func (*State) Progress ¶
func (s *State) Progress(ctx context.Context, userID int64, key core.RatingKey, offset, duration time.Duration, state string) error
Progress records a timeline update. state is Plex's timeline state ("playing", "paused", "stopped", "buffering"). See the package doc for the 5%/90% rules. A duration of 0 keeps the previously known duration; with no known duration the offset is kept as a resume point and nothing is ever marked watched by progress alone.
On a clustered node a Progress the database cannot take within a short timeout is held and written later (see Options.Availability); it then returns nil.
func (*State) ScrobbleMany ¶
ScrobbleMany marks several items watched in one transaction — the Plex front end expands a show or season scrobble into its episode keys and calls this. Each key gets a history entry and an EventScrobble.
func (*State) Subscribe ¶
Subscribe registers fn for state events (scrobbles, progress, ratings) so Trakt sync, recommendations, prefetch etc. can react. It returns an unsubscribe function. Handlers run synchronously after the change is committed; a panicking handler is recovered and logged.
func (*State) Unscrobble ¶
Unscrobble marks an item unwatched: view count and offset clear. The user's rating and history log are kept (history is a log, not status).
func (*State) UnscrobbleMany ¶
UnscrobbleMany marks several items unwatched in one transaction (show or season "mark unwatched").