Server changes/Sonarr
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -33,6 +34,27 @@ type LibrarySource interface {
|
||||
LibraryCandidates(ctx context.Context, genres []string, limit int) ([]json.RawMessage, error)
|
||||
}
|
||||
|
||||
// CuratedLibrarySource is the optional richer catalogue query used for server-authored
|
||||
// collections. Keeping it separate preserves compatibility with simpler test sources.
|
||||
type CuratedLibrarySource interface {
|
||||
CuratedCandidates(
|
||||
ctx context.Context,
|
||||
itemTypes, genres, studios []string,
|
||||
limit int,
|
||||
) ([]json.RawMessage, error)
|
||||
}
|
||||
|
||||
// CuratedRow defines one reusable server-side shelf. Filtering determines membership;
|
||||
// the user's profile determines both item order and shelf order.
|
||||
type CuratedRow struct {
|
||||
ID string
|
||||
Title string
|
||||
Kind string
|
||||
ItemTypes []string
|
||||
Genres []string
|
||||
Studios []string
|
||||
}
|
||||
|
||||
type Engine struct {
|
||||
source Source
|
||||
log *slog.Logger
|
||||
@@ -47,6 +69,7 @@ type Engine struct {
|
||||
// screen rather than a wall of near-duplicates.
|
||||
MaxSimilarRows int
|
||||
RowSize int
|
||||
CuratedRows []CuratedRow
|
||||
}
|
||||
|
||||
func NewEngine(source Source, log *slog.Logger) *Engine {
|
||||
@@ -56,6 +79,29 @@ func NewEngine(source Source, log *slog.Logger) *Engine {
|
||||
MinRowItems: 4,
|
||||
MaxSimilarRows: 2,
|
||||
RowSize: 20,
|
||||
CuratedRows: []CuratedRow{
|
||||
{
|
||||
ID: "curated:apple-tv",
|
||||
Title: "Apple TV+ Shows",
|
||||
Kind: "shows",
|
||||
ItemTypes: []string{"Series"},
|
||||
Studios: []string{"Apple TV+", "Apple TV Plus", "Apple Studios"},
|
||||
},
|
||||
{
|
||||
ID: "curated:drama-shows",
|
||||
Title: "Drama TV Shows",
|
||||
Kind: "shows",
|
||||
ItemTypes: []string{"Series"},
|
||||
Genres: []string{"Drama"},
|
||||
},
|
||||
{
|
||||
ID: "curated:comedy-shows",
|
||||
Title: "Comedy TV Shows",
|
||||
Kind: "shows",
|
||||
ItemTypes: []string{"Series"},
|
||||
Genres: []string{"Comedy"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,25 +122,77 @@ func (e *Engine) BuildRows(ctx context.Context, cred emby.Credentials) ([]Row, e
|
||||
}
|
||||
|
||||
profile := BuildProfile(history, favorites)
|
||||
if profile.IsEmpty() {
|
||||
// A brand-new user has nothing to recommend from. No rows is the honest answer.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
rows := make([]Row, 0, e.MaxSimilarRows+1)
|
||||
for _, seed := range e.seedsFor(profile) {
|
||||
row, ok := e.similarRow(ctx, cred, profile, seed)
|
||||
if ok {
|
||||
rows := make([]Row, 0, e.MaxSimilarRows+1+len(e.CuratedRows))
|
||||
if !profile.IsEmpty() {
|
||||
for _, seed := range e.seedsFor(profile) {
|
||||
row, ok := e.similarRow(ctx, cred, profile, seed)
|
||||
if ok {
|
||||
rows = append(rows, row)
|
||||
}
|
||||
}
|
||||
|
||||
if row, ok := e.historyRow(ctx, cred, profile); ok {
|
||||
rows = append(rows, row)
|
||||
}
|
||||
}
|
||||
|
||||
if row, ok := e.historyRow(ctx, cred, profile); ok {
|
||||
rows = append(rows, row)
|
||||
}
|
||||
rows = append(rows, e.buildCuratedRows(ctx, profile)...)
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func (e *Engine) buildCuratedRows(ctx context.Context, profile Profile) []Row {
|
||||
library, ok := e.Library.(CuratedLibrarySource)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
type rankedDefinition struct {
|
||||
definition CuratedRow
|
||||
affinity float64
|
||||
order int
|
||||
}
|
||||
definitions := make([]rankedDefinition, 0, len(e.CuratedRows))
|
||||
for order, definition := range e.CuratedRows {
|
||||
definitions = append(definitions, rankedDefinition{
|
||||
definition: definition,
|
||||
affinity: profile.CollectionAffinity(definition.Genres, definition.Studios),
|
||||
order: order,
|
||||
})
|
||||
}
|
||||
sort.SliceStable(definitions, func(i, j int) bool {
|
||||
if definitions[i].affinity != definitions[j].affinity {
|
||||
return definitions[i].affinity > definitions[j].affinity
|
||||
}
|
||||
return definitions[i].order < definitions[j].order
|
||||
})
|
||||
|
||||
rows := make([]Row, 0, len(definitions))
|
||||
for _, ranked := range definitions {
|
||||
definition := ranked.definition
|
||||
raws, err := library.CuratedCandidates(
|
||||
ctx,
|
||||
definition.ItemTypes,
|
||||
definition.Genres,
|
||||
definition.Studios,
|
||||
e.RowSize*6,
|
||||
)
|
||||
if err != nil {
|
||||
e.log.Warn("curated row failed", "row", definition.ID, "error", err)
|
||||
continue
|
||||
}
|
||||
items := RankCollection(profile, Decode(raws), e.RowSize)
|
||||
if len(items) < e.MinRowItems {
|
||||
continue
|
||||
}
|
||||
rows = append(rows, Row{
|
||||
ID: definition.ID,
|
||||
Title: definition.Title,
|
||||
Kind: definition.Kind,
|
||||
Items: Raws(items),
|
||||
})
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
// gatherSignals reads what the user has watched and favourited, in parallel.
|
||||
func (e *Engine) gatherSignals(ctx context.Context, cred emby.Credentials) (history, favorites []Item, err error) {
|
||||
var (
|
||||
|
||||
Reference in New Issue
Block a user