This commit is contained in:
ponzischeme89
2026-08-23 09:40:45 +12:00
parent 9a1f44da46
commit 766cea2199
19 changed files with 828 additions and 164 deletions
+37
View File
@@ -109,6 +109,11 @@ type Profile struct {
// good reason to *tell* someone about a title and a poor reason to rank by it: two
// films sharing an actor are often nothing alike.
PersonWeights map[string]float64
// DecadeWeights and ReasonEvidence retain just enough of the source history for the
// explanation layer to say why a particular title fits. They do not participate in
// ranking: ordering and wording remain deliberately separate concerns.
DecadeWeights map[int]float64
ReasonEvidence []ReasonEvidence
// Seen holds item ids *and* series ids already watched or in progress, so a
// recommendation never suggests something the user is already partway through.
Seen map[string]bool
@@ -116,6 +121,11 @@ type Profile struct {
Seeds []Seed
}
type ReasonEvidence struct {
Item Item
Favourite bool
}
func (p Profile) IsEmpty() bool { return len(p.GenreWeights) == 0 && len(p.Seeds) == 0 }
// Decode parses raw Emby items, keeping the original payload attached.
@@ -141,14 +151,26 @@ func BuildProfile(history, favorites []Item) Profile {
GenreWeights: map[string]float64{},
StudioWeights: map[string]float64{},
PersonWeights: map[string]float64{},
DecadeWeights: map[int]float64{},
Seen: map[string]bool{},
SeenTitles: map[string]bool{},
}
seedSeen := map[string]bool{}
tasteSeen := map[string]bool{}
reasonSeen := map[string]bool{}
for i, item := range history {
profile.markSeen(item)
reasonItem := item
reasonItem.Raw = nil
reasonID := item.ID
if item.SeriesID != "" {
reasonID = item.SeriesID
}
if reasonID != "" && !reasonSeen[reasonID] {
reasonSeen[reasonID] = true
profile.ReasonEvidence = append(profile.ReasonEvidence, ReasonEvidence{Item: reasonItem})
}
// Several episodes of one series are evidence for one taste, not several
// independent tastes. Keep the newest occurrence's recency weight and still
@@ -176,6 +198,15 @@ func BuildProfile(history, favorites []Item) Profile {
for _, item := range favorites {
profile.absorb(item, favoriteWeight)
reasonItem := item
reasonItem.Raw = nil
reasonID := item.ID
if reasonID != "" && !reasonSeen[reasonID] {
reasonSeen[reasonID] = true
profile.ReasonEvidence = append(profile.ReasonEvidence, ReasonEvidence{
Item: reasonItem, Favourite: true,
})
}
}
return profile
}
@@ -223,6 +254,12 @@ func (p *Profile) absorbTaste(item Item, weight float64) {
p.PersonWeights[name] += weight
}
}
if item.ProductionYear > 0 {
if p.DecadeWeights == nil {
p.DecadeWeights = map[int]float64{}
}
p.DecadeWeights[item.ProductionYear/10*10] += weight
}
}
// isExplainablePerson keeps the cast list down to the roles a viewer would recognise as