Files
memby/server/internal/api/playback_log.go
T

78 lines
2.3 KiB
Go
Raw Normal View History

2026-08-06 22:33:56 +12:00
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
}