Files
memby/server/internal/credits/fingerprint.go
T

65 lines
2.7 KiB
Go
Raw Normal View History

2026-08-15 09:23:26 +12:00
package credits
import (
"crypto/sha256"
"encoding/hex"
"strconv"
"strings"
"time"
)
// Identifying a media *version*, not an item.
//
// An Emby item id survives a file being replaced, which is exactly the case this subsystem
// must not get wrong: Sonarr swapping a 720p rip for a 1080p one leaves the id alone, and a
// marker measured against the old file would put Skip Credits somewhere arbitrary in the new
// one. Keying the marker on item *and* fingerprint means a replaced file simply stops
// matching — the episode becomes a candidate again, and nothing had to notice the swap or
// run an invalidation pass to make that happen.
// MediaVersion is what a fingerprint is computed from. Every field is optional except the
// item id: a source that can only offer runtime still produces a usable fingerprint, it is
// just a coarser one.
type MediaVersion struct {
ItemID string
RuntimeMs int64
SizeBytes int64
ModifiedAt time.Time
// ETag is Emby's own version token where it offers one. It is the strongest field here
// because it changes for reasons the other three can miss — a remux to the same length
// and near-enough the same size.
ETag string
}
// Fingerprint is a short, stable digest of a media version.
//
// Truncated to sixteen bytes because it is an equality key rather than a security claim: it
// is only ever compared against another fingerprint of the same item, and a full digest
// would double the width of the busiest index in the schema for no gain.
func Fingerprint(version MediaVersion) string {
parts := []string{
strings.TrimSpace(version.ItemID),
strconv.FormatInt(version.RuntimeMs, 10),
strconv.FormatInt(version.SizeBytes, 10),
strings.TrimSpace(version.ETag),
}
if !version.ModifiedAt.IsZero() {
// Second resolution. Filesystems and Emby disagree about sub-second timestamps often
// enough that finer granularity would invalidate markers on files nothing had
// touched, which is the expensive direction to be wrong in.
parts = append(parts, strconv.FormatInt(version.ModifiedAt.UTC().Unix(), 10))
}
sum := sha256.Sum256([]byte(strings.Join(parts, "|")))
return hex.EncodeToString(sum[:16])
}
// Weak reports a fingerprint built from too little to detect a file swap.
//
// Runtime alone does not distinguish a re-encode of the same episode, so a marker stored
// against a weak fingerprint is one that could survive a replacement it should not have. The
// service refuses to store those rather than storing a marker it cannot invalidate — the
// alternative is a wrong Skip Credits position that nothing will ever correct.
func (v MediaVersion) Weak() bool {
return v.SizeBytes <= 0 && strings.TrimSpace(v.ETag) == "" && v.ModifiedAt.IsZero()
}