Publish current app and server
This commit is contained in:
@@ -12,6 +12,7 @@ import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
@@ -37,6 +38,10 @@ type Item struct {
|
||||
Genres []string `json:"Genres"`
|
||||
CommunityRating float64 `json:"CommunityRating"`
|
||||
RunTimeTicks int64 `json:"RunTimeTicks"`
|
||||
OfficialRating string `json:"OfficialRating"`
|
||||
CollectionName string `json:"CollectionName"`
|
||||
PremiereDate string `json:"PremiereDate"`
|
||||
DateCreated string `json:"DateCreated"`
|
||||
IndexNumber int `json:"IndexNumber"`
|
||||
ParentIndexNumber int `json:"ParentIndexNumber"`
|
||||
Container string `json:"Container"`
|
||||
@@ -47,6 +52,7 @@ type Item struct {
|
||||
Studios []struct {
|
||||
Name string `json:"Name"`
|
||||
} `json:"Studios"`
|
||||
People []Person `json:"People"`
|
||||
UserData struct {
|
||||
Played bool `json:"Played"`
|
||||
PlayCount int `json:"PlayCount"`
|
||||
@@ -99,6 +105,10 @@ type Seed struct {
|
||||
type Profile struct {
|
||||
GenreWeights map[string]float64
|
||||
StudioWeights map[string]float64
|
||||
// PersonWeights is read only by the explanation layer, never by Score. Casting is a
|
||||
// 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
|
||||
// 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
|
||||
@@ -130,6 +140,7 @@ func BuildProfile(history, favorites []Item) Profile {
|
||||
profile := Profile{
|
||||
GenreWeights: map[string]float64{},
|
||||
StudioWeights: map[string]float64{},
|
||||
PersonWeights: map[string]float64{},
|
||||
Seen: map[string]bool{},
|
||||
SeenTitles: map[string]bool{},
|
||||
}
|
||||
@@ -201,6 +212,27 @@ func (p *Profile) absorbTaste(item Item, weight float64) {
|
||||
p.StudioWeights[s] += weight * 0.4
|
||||
}
|
||||
}
|
||||
for _, person := range item.People {
|
||||
if !isExplainablePerson(person.Type) {
|
||||
continue
|
||||
}
|
||||
if name := strings.TrimSpace(person.Name); name != "" {
|
||||
if p.PersonWeights == nil {
|
||||
p.PersonWeights = map[string]float64{}
|
||||
}
|
||||
p.PersonWeights[name] += weight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// isExplainablePerson keeps the cast list down to the roles a viewer would recognise as
|
||||
// a reason. A gaffer in common is not why anyone picks a film.
|
||||
func isExplainablePerson(role string) bool {
|
||||
switch strings.ToLower(strings.TrimSpace(role)) {
|
||||
case "actor", "director", "writer":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// TopGenres returns the n heaviest genres, highest first. Ties break alphabetically so
|
||||
@@ -263,7 +295,20 @@ func (p Profile) Score(candidate Item) float64 {
|
||||
// A mild quality nudge, capped so a beloved genre still beats a well-rated stranger.
|
||||
ratingScore := candidate.CommunityRating / 10 * 0.5
|
||||
|
||||
return genreScore + studioScore + ratingScore
|
||||
// New catalogue arrivals should surface without overpowering established taste.
|
||||
// Production year is consistently available from both the imported library and
|
||||
// Emby, unlike DateCreated on deliberately narrow recommendation payloads.
|
||||
var freshnessScore float64
|
||||
age := time.Now().Year() - candidate.ProductionYear
|
||||
switch {
|
||||
case candidate.ProductionYear <= 0:
|
||||
case age <= 1:
|
||||
freshnessScore = 0.25
|
||||
case age <= 3:
|
||||
freshnessScore = 0.12
|
||||
}
|
||||
|
||||
return genreScore + studioScore + ratingScore + freshnessScore
|
||||
}
|
||||
|
||||
// CollectionAffinity decides which curated shelf appears first for this user.
|
||||
|
||||
Reference in New Issue
Block a user