Publish current app and server

This commit is contained in:
ponzischeme89
2026-08-02 22:10:19 +12:00
parent a265636139
commit 1ed180c739
203 changed files with 23933 additions and 2788 deletions
+46 -1
View File
@@ -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.