0.2.64 update
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
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"
|
||||
}
|
||||
Reference in New Issue
Block a user