package api import ( "strconv" "sync" "time" ) // rememberedTitles bounds the title memory below. A household plays a handful of things // at once; this is generous enough that a report always finds its title and small enough // that a hostile client cannot grow it into a leak. const rememberedTitles = 256 // playbackTitles remembers what each item was called when the player asked to play it. // // Progress and stop reports carry an item id and nothing else — the client already knows // the title and has no reason to send it back — so without this every playback event // after the first reads as an opaque GUID. Deliberately in memory and deliberately lossy: // a restarted gateway logs an id until the next play, which is a better trade than a // round trip to Emby on a report the viewer never sees. type playbackTitles struct { mu sync.Mutex titles map[string]string order []string } func (p *playbackTitles) remember(itemID, title string) { if itemID == "" || title == "" { return } p.mu.Lock() defer p.mu.Unlock() if p.titles == nil { p.titles = make(map[string]string, rememberedTitles) } if _, known := p.titles[itemID]; !known { p.order = append(p.order, itemID) if len(p.order) > rememberedTitles { delete(p.titles, p.order[0]) p.order = p.order[1:] } } p.titles[itemID] = title } // name falls back to the id rather than an empty field: a line that says which item it // could not name is still usable. func (p *playbackTitles) name(itemID string) string { p.mu.Lock() defer p.mu.Unlock() if title, known := p.titles[itemID]; known { return title } return itemID } // millisecondDuration renders a playhead as "1h42m30s" rather than a seven-digit // millisecond count, which no one can read at a glance. func millisecondDuration(milliseconds int64) time.Duration { return (time.Duration(max64(milliseconds, 0)) * time.Millisecond).Round(time.Second) } // watchedPercent is how a stop is read at a glance: "did they finish it, or give up?" func watchedPercent(positionMs, durationMs int64) string { if durationMs <= 0 { return "unknown" } percent := max64(positionMs, 0) * 100 / durationMs return strconv.FormatInt(min64(percent, 100), 10) + "%" } func min64(v, ceiling int64) int64 { if v > ceiling { return ceiling } return v }