Big changes

This commit is contained in:
ponzischeme89
2026-07-29 15:26:27 +12:00
parent 8d6cf2f5a1
commit 70914400b4
62 changed files with 7501 additions and 744 deletions
+94 -16
View File
@@ -20,14 +20,16 @@ var ErrNotFound = errors.New("store: session not found")
var ErrDeviceLimit = errors.New("store: device limit reached")
type Session struct {
TokenHash []byte
EmbyUserID string
EmbyToken string
Username string
ServerID string
DeviceID string
DeviceName string
LastSeenAt time.Time
TokenHash []byte
EmbyUserID string
EmbyToken string
Username string
ServerID string
DeviceID string
DeviceName string
ClientVersion string
ClientProtocol string
LastSeenAt time.Time
}
type Store struct {
@@ -50,6 +52,58 @@ func (s *Store) Close() { s.pool.Close() }
func (s *Store) Ping(ctx context.Context) error { return s.pool.Ping(ctx) }
// RecordSearch stores a normalized query for future per-user ranking analysis.
func (s *Store) RecordSearch(ctx context.Context, userID, query string) error {
_, err := s.pool.Exec(ctx,
`WITH inserted AS (
INSERT INTO search_history (emby_user_id, query) VALUES ($1, $2)
RETURNING id
)
DELETE FROM search_history
WHERE emby_user_id = $1
AND occurred_at < now() - interval '30 days'`,
userID, query)
return err
}
// RecentSearches returns a user's distinct queries in most-recently-used order.
// Case-only duplicates collapse to the spelling used most recently.
func (s *Store) RecentSearches(
ctx context.Context,
userID string,
since time.Time,
limit int,
) ([]string, error) {
rows, err := s.pool.Query(ctx, `
SELECT query
FROM (
SELECT DISTINCT ON (lower(query)) query, occurred_at
FROM search_history
WHERE emby_user_id = $1 AND occurred_at >= $2
ORDER BY lower(query), occurred_at DESC
) AS latest
ORDER BY occurred_at DESC
LIMIT $3`,
userID, since, limit)
if err != nil {
return nil, fmt.Errorf("store: recent searches: %w", err)
}
defer rows.Close()
queries := make([]string, 0, limit)
for rows.Next() {
var query string
if err := rows.Scan(&query); err != nil {
return nil, fmt.Errorf("store: scan recent search: %w", err)
}
queries = append(queries, query)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("store: read recent searches: %w", err)
}
return queries, nil
}
// Migrate applies the schema. It is idempotent, so it runs on every boot.
func (s *Store) Migrate(ctx context.Context) error {
if _, err := s.pool.Exec(ctx, schema); err != nil {
@@ -94,18 +148,21 @@ func (s *Store) CreateSession(ctx context.Context, sess Session, maxClients int)
_, err = tx.Exec(ctx, `
INSERT INTO sessions (
token_hash, emby_user_id, emby_token, username, server_id, device_id, device_name
token_hash, emby_user_id, emby_token, username, server_id, device_id, device_name,
client_version, client_protocol
)
VALUES ($1, $2, $3, $4, $5, $6, $7)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
ON CONFLICT (emby_user_id, device_id) DO UPDATE SET
token_hash = EXCLUDED.token_hash,
emby_token = EXCLUDED.emby_token,
username = EXCLUDED.username,
server_id = EXCLUDED.server_id,
device_name = EXCLUDED.device_name,
client_version = EXCLUDED.client_version,
client_protocol = EXCLUDED.client_protocol,
last_seen_at = now()`,
sess.TokenHash, sess.EmbyUserID, sess.EmbyToken, sess.Username,
sess.ServerID, sess.DeviceID, sess.DeviceName)
sess.ServerID, sess.DeviceID, sess.DeviceName, sess.ClientVersion, sess.ClientProtocol)
if err != nil {
return nil, 0, fmt.Errorf("store: create session: %w", err)
}
@@ -121,10 +178,12 @@ func (s *Store) CreateSession(ctx context.Context, sess Session, maxClients int)
func (s *Store) SessionByTokenHash(ctx context.Context, hash []byte) (Session, error) {
var sess Session
err := s.pool.QueryRow(ctx, `
SELECT token_hash, emby_user_id, emby_token, username, server_id, device_id, device_name, last_seen_at
SELECT token_hash, emby_user_id, emby_token, username, server_id, device_id,
device_name, client_version, client_protocol, last_seen_at
FROM sessions WHERE token_hash = $1`, hash).
Scan(&sess.TokenHash, &sess.EmbyUserID, &sess.EmbyToken, &sess.Username,
&sess.ServerID, &sess.DeviceID, &sess.DeviceName, &sess.LastSeenAt)
&sess.ServerID, &sess.DeviceID, &sess.DeviceName, &sess.ClientVersion,
&sess.ClientProtocol, &sess.LastSeenAt)
if errors.Is(err, pgx.ErrNoRows) {
return Session{}, ErrNotFound
}
@@ -141,6 +200,23 @@ func (s *Store) Touch(ctx context.Context, hash []byte) error {
return err
}
// UpdateSessionClientIdentity remembers the last non-empty identity supplied by a TV.
// Headerless image requests can then still be attributed to the correct app build.
func (s *Store) UpdateSessionClientIdentity(
ctx context.Context,
hash []byte,
version, protocol string,
) error {
_, err := s.pool.Exec(ctx, `
UPDATE sessions
SET client_version = CASE WHEN $2 <> '' THEN $2 ELSE client_version END,
client_protocol = CASE WHEN $3 <> '' THEN $3 ELSE client_protocol END,
last_seen_at = now()
WHERE token_hash = $1`,
hash, version, protocol)
return err
}
func (s *Store) DeleteSession(ctx context.Context, hash []byte) error {
_, err := s.pool.Exec(ctx, `DELETE FROM sessions WHERE token_hash = $1`, hash)
return err
@@ -176,10 +252,11 @@ func (s *Store) TrimSessionsToLimit(ctx context.Context, maxClients int) ([]Sess
AND ranked.device_rank > $1
RETURNING current.token_hash, current.emby_user_id, current.emby_token,
current.username, current.server_id, current.device_id,
current.device_name, current.last_seen_at
current.device_name, current.client_version, current.client_protocol,
current.last_seen_at
)
SELECT token_hash, emby_user_id, emby_token, username, server_id,
device_id, device_name, last_seen_at
device_id, device_name, client_version, client_protocol, last_seen_at
FROM retired`,
maxClients,
)
@@ -193,7 +270,8 @@ func (s *Store) TrimSessionsToLimit(ctx context.Context, maxClients int) ([]Sess
var sess Session
if err := rows.Scan(
&sess.TokenHash, &sess.EmbyUserID, &sess.EmbyToken, &sess.Username,
&sess.ServerID, &sess.DeviceID, &sess.DeviceName, &sess.LastSeenAt,
&sess.ServerID, &sess.DeviceID, &sess.DeviceName, &sess.ClientVersion,
&sess.ClientProtocol, &sess.LastSeenAt,
); err != nil {
return nil, fmt.Errorf("store: scan trimmed session: %w", err)
}