Publish current app and server
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user