65 lines
2.7 KiB
Go
65 lines
2.7 KiB
Go
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()
|
|
}
|