0.3.06
This commit is contained in:
@@ -25,6 +25,7 @@ type PreparedEvidence struct {
|
||||
ItemID string `json:"itemId"`
|
||||
Title string `json:"title"`
|
||||
Genres []string `json:"genres,omitempty"`
|
||||
Completed bool `json:"completed,omitempty"`
|
||||
}
|
||||
|
||||
type PreparedTitleAffinity struct {
|
||||
@@ -128,7 +129,7 @@ func (e *Engine) PrepareForYou(
|
||||
index := newCatalogueIndex(catalogue)
|
||||
evidenceByGenre := map[string][]PreparedEvidence{}
|
||||
evidenceSeen := map[string]bool{}
|
||||
addCompletedEvidence := func(item Item, sessionID string) {
|
||||
addCompletedEvidence := func(item Item, sessionID string, completed bool) {
|
||||
itemID := item.ID
|
||||
title := item.Name
|
||||
if strings.EqualFold(item.Type, "Episode") &&
|
||||
@@ -152,6 +153,7 @@ func (e *Engine) PrepareForYou(
|
||||
ItemID: itemID,
|
||||
Title: title,
|
||||
Genres: append([]string(nil), item.Genres...),
|
||||
Completed: completed,
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -160,7 +162,7 @@ func (e *Engine) PrepareForYou(
|
||||
// explanation pool breadth even when Tracearr title matching is sparse.
|
||||
for _, item := range history {
|
||||
if item.UserData.Played {
|
||||
addCompletedEvidence(item, "")
|
||||
addCompletedEvidence(item, "", !strings.EqualFold(item.Type, "Episode"))
|
||||
}
|
||||
}
|
||||
titleAffinity := map[string]PreparedTitleAffinity{}
|
||||
@@ -263,7 +265,9 @@ func (e *Engine) PrepareForYou(
|
||||
}
|
||||
|
||||
if completion >= 0.9 {
|
||||
addCompletedEvidence(item, session.ID)
|
||||
addCompletedEvidence(
|
||||
item, session.ID, !strings.EqualFold(session.MediaType, "episode"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -559,49 +563,46 @@ func explainPreparedRecommendation(
|
||||
evidenceByGenre map[string][]PreparedEvidence,
|
||||
completedReasonCounts map[string]int,
|
||||
) (reason, label, kind, genre string, evidence PreparedEvidence) {
|
||||
for _, wanted := range profile.TopGenres(5) {
|
||||
for _, candidateGenre := range item.Genres {
|
||||
if strings.EqualFold(wanted, candidateGenre) {
|
||||
genre = candidateGenre
|
||||
options := evidenceByGenre[strings.ToLower(strings.TrimSpace(wanted))]
|
||||
strong := make([]PreparedEvidence, 0, len(options))
|
||||
for _, option := range options {
|
||||
if strongEvidenceMatch(item, option) {
|
||||
strong = append(strong, option)
|
||||
}
|
||||
}
|
||||
if len(strong) > 0 {
|
||||
evidence = strong[stableEvidenceIndex(item.ID, len(strong))]
|
||||
}
|
||||
break
|
||||
strong := make([]PreparedEvidence, 0)
|
||||
seenEvidence := map[string]bool{}
|
||||
for _, candidateGenre := range item.Genres {
|
||||
options := evidenceByGenre[strings.ToLower(strings.TrimSpace(candidateGenre))]
|
||||
for _, option := range options {
|
||||
if seenEvidence[option.ItemID] || !strongEvidenceMatch(item, option) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if genre != "" {
|
||||
break
|
||||
seenEvidence[option.ItemID] = true
|
||||
strong = append(strong, option)
|
||||
}
|
||||
}
|
||||
// Keep specific evidence prominent without letting it monopolise a row. One third
|
||||
// of otherwise eligible cards deliberately uses the broader genre explanation,
|
||||
// and no completed title can explain more than four candidates in a prepared pool.
|
||||
useCompleted := evidence.Title != "" &&
|
||||
stableEvidenceIndex("reason-kind:"+item.ID, 3) != 0 &&
|
||||
completedReasonCounts[evidence.ItemID] < 4
|
||||
if len(strong) > 0 {
|
||||
evidence = strong[stableEvidenceIndex(item.ID, len(strong))]
|
||||
}
|
||||
useTitle := evidence.Title != "" && completedReasonCounts[evidence.ItemID] < 4
|
||||
switch {
|
||||
case useTitle:
|
||||
verb := "watched "
|
||||
kind = "recent-title"
|
||||
if evidence.Completed {
|
||||
verb, kind = "finished ", "completed-title"
|
||||
}
|
||||
reason = "Because you " + verb + evidence.Title
|
||||
completedReasonCounts[evidence.ItemID]++
|
||||
case browsed:
|
||||
reason, kind = "You explored this recently", "browsed"
|
||||
evidence = PreparedEvidence{}
|
||||
case useCompleted:
|
||||
reason, kind = "Because you finished "+evidence.Title, "completed-title"
|
||||
completedReasonCounts[evidence.ItemID]++
|
||||
case genre != "":
|
||||
reason, kind = "Matches your "+genre+" viewing", "genre"
|
||||
evidence = PreparedEvidence{}
|
||||
case len(profile.Seeds) > 0:
|
||||
reason, kind = "Inspired by "+profile.Seeds[0].Name, "recent-title"
|
||||
evidence = PreparedEvidence{}
|
||||
default:
|
||||
reason, kind = "Matches your recent viewing", "generic"
|
||||
evidence = PreparedEvidence{}
|
||||
selected := strongestAvailableReason(profile, item, completedReasonCounts)
|
||||
reason, kind, genre = selected.Text, selected.Kind, selected.Genre
|
||||
if selected.SourceID != "" {
|
||||
completedReasonCounts[selected.SourceID]++
|
||||
evidence = PreparedEvidence{
|
||||
ItemID: selected.SourceID, Title: selected.SourceTitle,
|
||||
Completed: selected.Kind == "completed-title",
|
||||
}
|
||||
} else {
|
||||
evidence = PreparedEvidence{}
|
||||
}
|
||||
}
|
||||
switch score := compatibilityScore(item, compatibility); {
|
||||
case score > 0.2:
|
||||
@@ -615,24 +616,22 @@ func explainPreparedRecommendation(
|
||||
return reason, label, kind, genre, evidence
|
||||
}
|
||||
|
||||
func strongEvidenceMatch(item Item, evidence PreparedEvidence) bool {
|
||||
shared := 0
|
||||
broadOnly := true
|
||||
for _, candidateGenre := range item.Genres {
|
||||
for _, evidenceGenre := range evidence.Genres {
|
||||
if !strings.EqualFold(strings.TrimSpace(candidateGenre), strings.TrimSpace(evidenceGenre)) {
|
||||
continue
|
||||
}
|
||||
shared++
|
||||
switch strings.ToLower(strings.TrimSpace(candidateGenre)) {
|
||||
case "action", "adventure", "comedy", "drama", "thriller":
|
||||
default:
|
||||
broadOnly = false
|
||||
}
|
||||
break
|
||||
func strongestAvailableReason(
|
||||
profile Profile,
|
||||
item Item,
|
||||
titleReasonCounts map[string]int,
|
||||
) personalReason {
|
||||
for _, reason := range personalReasons(profile, item) {
|
||||
if reason.SourceID == "" || titleReasonCounts[reason.SourceID] < 4 {
|
||||
return reason
|
||||
}
|
||||
}
|
||||
return shared >= 2 || shared == 1 && !broadOnly
|
||||
return personalReason{Text: "Recommended from your library", Kind: "generic"}
|
||||
}
|
||||
|
||||
func strongEvidenceMatch(item Item, evidence PreparedEvidence) bool {
|
||||
shared, specific := sharedGenreCount(item.Genres, evidence.Genres)
|
||||
return shared >= 2 || specific >= 1
|
||||
}
|
||||
|
||||
func stableEvidenceIndex(itemID string, size int) int {
|
||||
|
||||
Reference in New Issue
Block a user