132 lines
4.6 KiB
Go
132 lines
4.6 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/ponzischeme89/memby/server/internal/scheduler"
|
|
)
|
|
|
|
// The gateway's side of credits discovery: where a discovered marker is read, and where the
|
|
// two live signals that drive it are picked up.
|
|
//
|
|
// Both signals are things the gateway was already being told. Nothing new is reported by a
|
|
// television and nothing new is written on the playback path — the subsystem is fed entirely
|
|
// by reports that existed before it did.
|
|
|
|
// discoveredCredits reads a stored marker for an item.
|
|
//
|
|
// One indexed lookup on the playback path, and it only happens for a title Emby had no
|
|
// chapter marker for. A failure is silence: this is an optional convenience on a film that is
|
|
// already playing, and there is nothing a viewer could do about an error.
|
|
func (s *Server) discoveredCredits(ctx context.Context, itemID string) (int64, bool) {
|
|
if s.credits == nil {
|
|
return 0, false
|
|
}
|
|
marker, found, err := s.credits.Marker(ctx, itemID)
|
|
if err != nil {
|
|
s.loggerFor(ctx).Debug("discovered credits unavailable", "item_id", itemID, "error", err)
|
|
return 0, false
|
|
}
|
|
if !found || marker.CreditsStartMs <= 0 {
|
|
return 0, false
|
|
}
|
|
return marker.CreditsStartMs, true
|
|
}
|
|
|
|
// noteCreditsPlayback feeds the two live signals from one playback report.
|
|
//
|
|
// The load gauge and the scan trigger are updated from the same call because they are two
|
|
// readings of one event, and splitting them across call sites is how the two come to
|
|
// disagree about what is playing.
|
|
//
|
|
// A started or progressing playback is the strongest evidence there is that an episode
|
|
// matters, but it does not scan anything yet: NotePlayback holds the candidate for its
|
|
// settling delay first, so an episode somebody opened and abandoned costs nothing.
|
|
func (s *Server) noteCreditsPlayback(ctx context.Context, phase, itemID, sessionKey string) {
|
|
if itemID == "" {
|
|
return
|
|
}
|
|
switch phase {
|
|
case "started":
|
|
s.creditsLoad.Playing(sessionKey)
|
|
if s.credits != nil && s.featureEnabled(ctx, featureEndCredits) {
|
|
s.credits.NotePlayback(ctx, itemID)
|
|
}
|
|
case "progress":
|
|
// Progress refreshes the gauge's timestamp. Without it a television that stopped
|
|
// reporting — a crash, a power cut — would hold the gauge busy for ever and
|
|
// speculative scanning would never run again.
|
|
s.creditsLoad.Playing(sessionKey)
|
|
case "stopped":
|
|
s.creditsLoad.Stopped(sessionKey)
|
|
// A playback that ended before its delay elapsed is exactly the case the delay
|
|
// exists for: somebody looked at the episode and changed their mind, and nothing
|
|
// should be read from disk on their account.
|
|
if s.credits != nil {
|
|
s.credits.AbandonPlayback(itemID)
|
|
}
|
|
}
|
|
}
|
|
|
|
// RegisterCreditsTasks declares the demand refresh, so its interval is the operator's to
|
|
// change and its last run is visible in the console beside every other background job.
|
|
//
|
|
// Refreshing on a schedule rather than continuously is the point: viewing behaviour changes
|
|
// over evenings, not seconds, and polling Tracearr any harder would cost more than the
|
|
// scanning it directs. Live playback is immediate and does not come through here.
|
|
func (s *Server) RegisterCreditsTasks(sched *scheduler.Scheduler) {
|
|
if sched == nil || s.credits == nil {
|
|
return
|
|
}
|
|
sched.Register(scheduler.Task{
|
|
ID: "credits-candidates",
|
|
Name: "Credits candidate refresh",
|
|
Group: "Library",
|
|
Description: "Rebuilds the credits-detection queue from what the household has " +
|
|
"recently been watching. Only episodes viewers are about to reach are queued.",
|
|
Interval: 10 * time.Minute,
|
|
Timeout: 2 * time.Minute,
|
|
RunOnStart: true,
|
|
Run: func(ctx context.Context) (string, error) {
|
|
if !s.featureEnabled(ctx, featureEndCredits) {
|
|
return "", nil
|
|
}
|
|
return s.credits.Refresh(ctx)
|
|
},
|
|
})
|
|
}
|
|
|
|
// creditsQueueDetail is the one line the admin console prints about the queue.
|
|
func (s *Server) creditsQueueDetail() string {
|
|
if s.credits == nil {
|
|
return ""
|
|
}
|
|
depth := s.credits.QueueDepth()
|
|
if depth == 0 {
|
|
return ""
|
|
}
|
|
return fmt.Sprintf("%d episode%s awaiting credits detection", depth, plural(depth))
|
|
}
|
|
|
|
// playbackSessionKey identifies one stream for the load gauge.
|
|
//
|
|
// The play session where Emby issued one, because that is what distinguishes two
|
|
// simultaneous plays from one television reconnecting. A device id is the fallback: an older
|
|
// build sends no play session, and counting every one of those as the same stream would make
|
|
// a household of old televisions read as permanently idle.
|
|
func playbackSessionKey(deviceID, playSessionID string) string {
|
|
if playSessionID != "" {
|
|
return playSessionID
|
|
}
|
|
return deviceID
|
|
}
|
|
|
|
func plural(count int) string {
|
|
if count == 1 {
|
|
return ""
|
|
}
|
|
return "s"
|
|
}
|