176 lines
5.8 KiB
Go
176 lines
5.8 KiB
Go
package store
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"fmt"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
type UserShow struct {
|
||
|
|
ItemID string `json:"itemId"`
|
||
|
|
Title string `json:"title"`
|
||
|
|
Year *int `json:"year,omitempty"`
|
||
|
|
ImageTag string `json:"imageTag,omitempty"`
|
||
|
|
AddedAt time.Time `json:"addedAt"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type NotificationPreferences struct {
|
||
|
|
Enabled bool `json:"enabled"`
|
||
|
|
ShowReturnAlerts bool `json:"showReturnAlerts"`
|
||
|
|
LeadDays int `json:"leadDays"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type UserNotification struct {
|
||
|
|
ID int64 `json:"id"`
|
||
|
|
Kind string `json:"kind"`
|
||
|
|
ItemID string `json:"itemId,omitempty"`
|
||
|
|
Title string `json:"title"`
|
||
|
|
Message string `json:"message"`
|
||
|
|
EventAt *time.Time `json:"eventAt,omitempty"`
|
||
|
|
CreatedAt time.Time `json:"createdAt"`
|
||
|
|
ReadAt *time.Time `json:"readAt,omitempty"`
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Store) SaveUserShow(ctx context.Context, userID string, show UserShow) error {
|
||
|
|
_, err := s.pool.Exec(ctx, `
|
||
|
|
INSERT INTO user_shows (emby_user_id, item_id, title, year, image_tag)
|
||
|
|
VALUES ($1, $2, $3, $4, $5)
|
||
|
|
ON CONFLICT (emby_user_id, item_id) DO UPDATE
|
||
|
|
SET title = EXCLUDED.title, year = EXCLUDED.year, image_tag = EXCLUDED.image_tag`,
|
||
|
|
userID, show.ItemID, show.Title, show.Year, show.ImageTag)
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
// SaveUserShowIfAbsent is the auto-follow path. Manual saves intentionally refresh
|
||
|
|
// metadata, while playback must only announce a show when it actually added it.
|
||
|
|
func (s *Store) SaveUserShowIfAbsent(ctx context.Context, userID string, show UserShow) (bool, error) {
|
||
|
|
result, err := s.pool.Exec(ctx, `
|
||
|
|
INSERT INTO user_shows (emby_user_id, item_id, title, year, image_tag)
|
||
|
|
VALUES ($1, $2, $3, $4, $5)
|
||
|
|
ON CONFLICT (emby_user_id, item_id) DO NOTHING`,
|
||
|
|
userID, show.ItemID, show.Title, show.Year, show.ImageTag)
|
||
|
|
if err != nil {
|
||
|
|
return false, err
|
||
|
|
}
|
||
|
|
return result.RowsAffected() == 1, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Store) DeleteUserShow(ctx context.Context, userID, itemID string) error {
|
||
|
|
_, err := s.pool.Exec(ctx, `
|
||
|
|
WITH removed AS (
|
||
|
|
DELETE FROM user_shows
|
||
|
|
WHERE emby_user_id = $1 AND item_id = $2
|
||
|
|
RETURNING item_id
|
||
|
|
)
|
||
|
|
UPDATE user_notifications SET dismissed_at = now()
|
||
|
|
WHERE emby_user_id = $1
|
||
|
|
AND item_id IN (SELECT item_id FROM removed)
|
||
|
|
AND dismissed_at IS NULL`,
|
||
|
|
userID, itemID)
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Store) UserShows(ctx context.Context, userID string) ([]UserShow, error) {
|
||
|
|
rows, err := s.pool.Query(ctx, `
|
||
|
|
SELECT item_id, title, year, image_tag, added_at
|
||
|
|
FROM user_shows WHERE emby_user_id = $1 ORDER BY added_at, lower(title)`, userID)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("store: list user shows: %w", err)
|
||
|
|
}
|
||
|
|
defer rows.Close()
|
||
|
|
shows := []UserShow{}
|
||
|
|
for rows.Next() {
|
||
|
|
var show UserShow
|
||
|
|
if err := rows.Scan(&show.ItemID, &show.Title, &show.Year, &show.ImageTag, &show.AddedAt); err != nil {
|
||
|
|
return nil, fmt.Errorf("store: scan user show: %w", err)
|
||
|
|
}
|
||
|
|
shows = append(shows, show)
|
||
|
|
}
|
||
|
|
return shows, rows.Err()
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Store) NotificationPreferences(ctx context.Context, userID string) (NotificationPreferences, error) {
|
||
|
|
prefs := NotificationPreferences{Enabled: true, ShowReturnAlerts: true, LeadDays: 7}
|
||
|
|
err := s.pool.QueryRow(ctx, `
|
||
|
|
SELECT enabled, show_return_alerts, lead_days
|
||
|
|
FROM user_notification_preferences WHERE emby_user_id = $1`, userID).
|
||
|
|
Scan(&prefs.Enabled, &prefs.ShowReturnAlerts, &prefs.LeadDays)
|
||
|
|
if err != nil && !isNoRows(err) {
|
||
|
|
return prefs, fmt.Errorf("store: notification preferences: %w", err)
|
||
|
|
}
|
||
|
|
return prefs, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Store) SetNotificationPreferences(
|
||
|
|
ctx context.Context, userID string, prefs NotificationPreferences,
|
||
|
|
) error {
|
||
|
|
if prefs.LeadDays < 1 {
|
||
|
|
prefs.LeadDays = 1
|
||
|
|
}
|
||
|
|
if prefs.LeadDays > 30 {
|
||
|
|
prefs.LeadDays = 30
|
||
|
|
}
|
||
|
|
_, err := s.pool.Exec(ctx, `
|
||
|
|
INSERT INTO user_notification_preferences
|
||
|
|
(emby_user_id, enabled, show_return_alerts, lead_days)
|
||
|
|
VALUES ($1, $2, $3, $4)
|
||
|
|
ON CONFLICT (emby_user_id) DO UPDATE SET
|
||
|
|
enabled = EXCLUDED.enabled,
|
||
|
|
show_return_alerts = EXCLUDED.show_return_alerts,
|
||
|
|
lead_days = EXCLUDED.lead_days,
|
||
|
|
updated_at = now()`,
|
||
|
|
userID, prefs.Enabled, prefs.ShowReturnAlerts, prefs.LeadDays)
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Store) UpsertNotification(
|
||
|
|
ctx context.Context, userID, sourceKey, kind, itemID, title, message string, eventAt *time.Time,
|
||
|
|
) error {
|
||
|
|
_, err := s.pool.Exec(ctx, `
|
||
|
|
INSERT INTO user_notifications
|
||
|
|
(emby_user_id, source_key, kind, item_id, title, message, event_at)
|
||
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||
|
|
ON CONFLICT (emby_user_id, source_key) DO NOTHING`,
|
||
|
|
userID, sourceKey, kind, itemID, title, message, eventAt)
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Store) UserNotifications(ctx context.Context, userID string) ([]UserNotification, error) {
|
||
|
|
rows, err := s.pool.Query(ctx, `
|
||
|
|
SELECT id, kind, item_id, title, message, event_at, created_at, read_at
|
||
|
|
FROM user_notifications
|
||
|
|
WHERE emby_user_id = $1 AND dismissed_at IS NULL
|
||
|
|
ORDER BY created_at DESC LIMIT 100`, userID)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("store: list notifications: %w", err)
|
||
|
|
}
|
||
|
|
defer rows.Close()
|
||
|
|
notifications := []UserNotification{}
|
||
|
|
for rows.Next() {
|
||
|
|
var notification UserNotification
|
||
|
|
if err := rows.Scan(
|
||
|
|
¬ification.ID, ¬ification.Kind, ¬ification.ItemID,
|
||
|
|
¬ification.Title, ¬ification.Message, ¬ification.EventAt,
|
||
|
|
¬ification.CreatedAt, ¬ification.ReadAt,
|
||
|
|
); err != nil {
|
||
|
|
return nil, fmt.Errorf("store: scan notification: %w", err)
|
||
|
|
}
|
||
|
|
notifications = append(notifications, notification)
|
||
|
|
}
|
||
|
|
return notifications, rows.Err()
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Store) MarkNotificationRead(ctx context.Context, userID string, id int64) error {
|
||
|
|
_, err := s.pool.Exec(ctx, `
|
||
|
|
UPDATE user_notifications SET read_at = COALESCE(read_at, now())
|
||
|
|
WHERE id = $1 AND emby_user_id = $2`, id, userID)
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Store) DismissNotification(ctx context.Context, userID string, id int64) error {
|
||
|
|
_, err := s.pool.Exec(ctx, `
|
||
|
|
UPDATE user_notifications SET dismissed_at = now()
|
||
|
|
WHERE id = $1 AND emby_user_id = $2`, id, userID)
|
||
|
|
return err
|
||
|
|
}
|