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
+116 -17
View File
@@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"github.com/jackc/pgx/v5"
@@ -17,6 +18,16 @@ type TracearrSessionKey struct {
SessionID string
}
type TracearrSessionSignal struct {
Fingerprint []byte
Terminal bool
}
type RecommendationIdentity struct {
TracearrUserID string
Username string
}
type TracearrSession struct {
ServerID string
SessionID string
@@ -65,7 +76,10 @@ type RecommendationProfile struct {
GenreAffinity json.RawMessage
TitleAffinity json.RawMessage
StudioAffinity json.RawMessage
ContextAffinity json.RawMessage
CodecOutcomes json.RawMessage
WeightedProfile json.RawMessage
AlgorithmVersion string
SignalsThrough *time.Time
BuiltAt time.Time
}
@@ -89,6 +103,8 @@ type ForYouCandidate struct {
type PreparedForYouItem struct {
ItemID string
BaseRank int
BaseScore float64
AffinityScore float64
Payload json.RawMessage
RuntimeMinutes int
CompatibilityScore float64
@@ -98,6 +114,7 @@ type PreparedForYouItem struct {
ReasonGenre string
ReasonSourceItemID string
ReasonSourceTitle string
ContextAffinity json.RawMessage
}
type ForYouStats struct {
@@ -151,6 +168,46 @@ func (s *Store) TracearrFingerprints(
return out, rows.Err()
}
func (s *Store) TracearrSessionSignals(
ctx context.Context,
keys []TracearrSessionKey,
) (map[TracearrSessionKey]TracearrSessionSignal, error) {
out := make(map[TracearrSessionKey]TracearrSessionSignal, len(keys))
if len(keys) == 0 {
return out, nil
}
servers := make([]string, 0, len(keys))
ids := make([]string, 0, len(keys))
for _, key := range keys {
servers = append(servers, key.ServerID)
ids = append(ids, key.SessionID)
}
rows, err := s.pool.Query(ctx, `
SELECT current.server_id, current.tracearr_session_id, current.source_fingerprint,
(current.watched OR current.stopped_at IS NOT NULL OR
lower(current.state) IN ('stopped', 'completed', 'complete', 'ended'))
FROM tracearr_sessions current
JOIN unnest($1::text[], $2::text[]) wanted(server_id, session_id)
ON current.server_id = wanted.server_id
AND current.tracearr_session_id = wanted.session_id`,
servers, ids)
if err != nil {
return nil, fmt.Errorf("store: tracearr session signals: %w", err)
}
defer rows.Close()
for rows.Next() {
var key TracearrSessionKey
var signal TracearrSessionSignal
if err := rows.Scan(
&key.ServerID, &key.SessionID, &signal.Fingerprint, &signal.Terminal,
); err != nil {
return nil, err
}
out[key] = signal
}
return out, rows.Err()
}
func (s *Store) UpsertTracearrSessions(
ctx context.Context,
sessions []TracearrSession,
@@ -354,7 +411,7 @@ func (s *Store) ActiveRecommendationUsers(ctx context.Context) ([]Session, error
rows, err := s.pool.Query(ctx, `
SELECT DISTINCT ON (emby_user_id)
token_hash, emby_user_id, emby_token, username, server_id, device_id,
device_name, client_version, client_protocol, last_seen_at
device_name, client_version, client_protocol, client_capabilities, last_seen_at
FROM sessions
ORDER BY emby_user_id, last_seen_at DESC`)
if err != nil {
@@ -367,7 +424,7 @@ func (s *Store) ActiveRecommendationUsers(ctx context.Context) ([]Session, error
if err := rows.Scan(
&session.TokenHash, &session.EmbyUserID, &session.EmbyToken, &session.Username,
&session.ServerID, &session.DeviceID, &session.DeviceName, &session.ClientVersion,
&session.ClientProtocol, &session.LastSeenAt,
&session.ClientProtocol, &session.ClientCapabilities, &session.LastSeenAt,
); err != nil {
return nil, err
}
@@ -400,6 +457,35 @@ func (s *Store) MarkAllForYouProfilesDirty(ctx context.Context) error {
return nil
}
func (s *Store) MarkForYouProfilesDirtyByTracearrIdentity(
ctx context.Context,
identities []RecommendationIdentity,
) (int64, error) {
if len(identities) == 0 {
return 0, nil
}
userIDs := make([]string, 0, len(identities))
usernames := make([]string, 0, len(identities))
for _, identity := range identities {
if identity.TracearrUserID != "" {
userIDs = append(userIDs, identity.TracearrUserID)
}
if identity.Username != "" {
usernames = append(usernames, strings.ToLower(identity.Username))
}
}
tag, err := s.pool.Exec(ctx, `
UPDATE recommendation_user_profiles
SET dirty_since = coalesce(dirty_since, now())
WHERE tracearr_user_id = ANY($1)
OR lower(tracearr_username) = ANY($2)`,
userIDs, usernames)
if err != nil {
return 0, fmt.Errorf("store: mark affected For You profiles dirty: %w", err)
}
return tag.RowsAffected(), nil
}
func (s *Store) MatchRecommendationUser(
ctx context.Context,
embyUserID, embyUsername, tracearrUserID, tracearrUsername string,
@@ -431,18 +517,18 @@ func (s *Store) MatchRecommendationUser(
func (s *Store) ForYouProfileTimes(
ctx context.Context,
userID string,
) (builtAt, poolBuiltAt, dirtySince *time.Time, err error) {
) (builtAt, poolBuiltAt, dirtySince *time.Time, algorithmVersion string, err error) {
err = s.pool.QueryRow(ctx, `
SELECT built_at, pool_built_at, dirty_since
SELECT built_at, pool_built_at, dirty_since, algorithm_version
FROM recommendation_user_profiles WHERE emby_user_id = $1`, userID).
Scan(&builtAt, &poolBuiltAt, &dirtySince)
Scan(&builtAt, &poolBuiltAt, &dirtySince, &algorithmVersion)
if errors.Is(err, pgx.ErrNoRows) {
return nil, nil, nil, nil
return nil, nil, nil, "", nil
}
if err != nil {
return nil, nil, nil, fmt.Errorf("store: For You freshness: %w", err)
return nil, nil, nil, "", fmt.Errorf("store: For You freshness: %w", err)
}
return builtAt, poolBuiltAt, dirtySince, nil
return builtAt, poolBuiltAt, dirtySince, algorithmVersion, nil
}
func (s *Store) SetForYouError(ctx context.Context, userID string, buildErr error) error {
@@ -474,10 +560,11 @@ func (s *Store) ReplaceForYouPool(
INSERT INTO recommendation_user_profiles (
emby_user_id, tracearr_user_id, tracearr_username, source_session_count,
mean_completion_ratio, typical_session_minutes, genre_affinity,
title_affinity, studio_affinity, codec_outcomes, signals_through,
built_at, pool_built_at, dirty_since, last_error
title_affinity, studio_affinity, context_affinity, codec_outcomes,
weighted_profile, algorithm_version, signals_through, built_at, pool_built_at, dirty_since, last_error
) VALUES (
$1,$2,$3,$4,$5,$6,$7::jsonb,$8::jsonb,$9::jsonb,$10::jsonb,$11,$12,$12,NULL,''
$1,$2,$3,$4,$5,$6,$7::jsonb,$8::jsonb,$9::jsonb,$10::jsonb,$11::jsonb,
$12::jsonb,$13,$14,$15,$15,NULL,''
)
ON CONFLICT (emby_user_id) DO UPDATE SET
tracearr_user_id = EXCLUDED.tracearr_user_id,
@@ -488,7 +575,10 @@ func (s *Store) ReplaceForYouPool(
genre_affinity = EXCLUDED.genre_affinity,
title_affinity = EXCLUDED.title_affinity,
studio_affinity = EXCLUDED.studio_affinity,
context_affinity = EXCLUDED.context_affinity,
codec_outcomes = EXCLUDED.codec_outcomes,
weighted_profile = EXCLUDED.weighted_profile,
algorithm_version = EXCLUDED.algorithm_version,
signals_through = EXCLUDED.signals_through,
built_at = EXCLUDED.built_at,
pool_built_at = EXCLUDED.pool_built_at,
@@ -498,7 +588,9 @@ func (s *Store) ReplaceForYouPool(
profile.SourceSessionCount, profile.MeanCompletionRatio,
profile.TypicalSessionMinutes, string(profile.GenreAffinity),
string(profile.TitleAffinity), string(profile.StudioAffinity),
string(profile.CodecOutcomes), profile.SignalsThrough, profile.BuiltAt)
string(profile.ContextAffinity), string(profile.CodecOutcomes),
string(profile.WeightedProfile), profile.AlgorithmVersion,
profile.SignalsThrough, profile.BuiltAt)
if err != nil {
return fmt.Errorf("store: write For You profile: %w", err)
}
@@ -544,15 +636,20 @@ func (s *Store) PreparedForYou(
minutes, limit int,
) ([]PreparedForYouItem, *time.Time, error) {
rows, err := s.pool.Query(ctx, `
SELECT fc.item_id, fc.base_rank, li.payload, fc.runtime_minutes,
SELECT fc.item_id, fc.base_rank, fc.base_score, fc.affinity_score,
li.payload, fc.runtime_minutes,
fc.compatibility_score, fc.compatibility_label,
fc.recommendation_reason, fc.reason_kind, fc.reason_genre,
fc.reason_source_item_id, fc.reason_source_title, p.pool_built_at
fc.reason_source_item_id, fc.reason_source_title, p.context_affinity,
p.pool_built_at
FROM for_you_candidates fc
JOIN library_items li ON li.id = fc.item_id
JOIN recommendation_user_profiles p ON p.emby_user_id = fc.emby_user_id
WHERE fc.emby_user_id = $1
AND ($2 = 0 OR (fc.runtime_minutes > 0 AND fc.runtime_minutes <= $2))
AND (
$2 = 0 OR fc.reason_kind = 'pick-up' OR
(fc.runtime_minutes > 0 AND fc.runtime_minutes <= $2)
)
ORDER BY fc.base_rank
LIMIT $3`,
userID, minutes, limit)
@@ -567,10 +664,12 @@ func (s *Store) PreparedForYou(
var item PreparedForYouItem
var rowBuiltAt *time.Time
if err := rows.Scan(
&item.ItemID, &item.BaseRank, &payload, &item.RuntimeMinutes,
&item.ItemID, &item.BaseRank, &item.BaseScore, &item.AffinityScore,
&payload, &item.RuntimeMinutes,
&item.CompatibilityScore, &item.CompatibilityLabel,
&item.RecommendationReason, &item.ReasonKind, &item.ReasonGenre,
&item.ReasonSourceItemID, &item.ReasonSourceTitle, &rowBuiltAt,
&item.ReasonSourceItemID, &item.ReasonSourceTitle, &item.ContextAffinity,
&rowBuiltAt,
); err != nil {
return nil, nil, err
}