* feat(invitations): add emailed pre-provisioned invitations
Admins can invite a specific person by email: the invitation pre-binds
role, access group, and library access, and the invitee only chooses a
password. Their email address becomes their username, so login gains an
email fallback (username lookup first, email column only on miss for
inputs that parse as a bare address).
- invitations table: single-use token (SHA-256 at rest) bound to one
address; a partial unique index makes resend-supersedes atomic; no
users row exists until accept, so a typo'd address can't squat a
username. Status is derived from timestamps, not stored.
- internal/invitations: repository, service, and branded email through
the shared internal/mail sender. When SMTP is off the claim URL is
returned for manual delivery instead of failing.
- Admin endpoints /admin/invitations (list/create/resend/revoke) beside
the existing invite-codes routes; public claim endpoints
/invitations/{token} (+/accept) rate-limited with the other auth
endpoints. Unknown/expired/revoked/used tokens are indistinguishable.
- Accept returns the same login response shape as signup, so clients
reuse their session plumbing.
Spec: docs/superpowers/specs/2026-07-27-invitations-and-onboarding-design.md
Plan: docs/superpowers/plans/2026-07-27-invitations-and-onboarding.md
Part of #215
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(web): add invitation admin tab, claim page, and household setup
- Admin → Users gains an Invitations tab: compose (email, role, access
group, libraries, note, first-profile and tour toggles), list with
derived status, resend, revoke. When the server has no SMTP the create
response's claim URL is surfaced for copy-paste instead of a fake
success.
- /invite/:token claim page: everything but the password was decided at
send time, so it asks for exactly one thing and lands the user signed
in. Expired/used links get an explanatory card, not a 404.
- /household-setup ("Who's watching?"): profile tiles plus the existing
ProfileEditorDialog, all through the existing /profiles endpoint —
no new backend. "Just me for now" is a first-class exit.
Part of #215
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(onboarding): add server-driven onboarding manifest and state
GET /onboarding/flow returns the ordered first-run tour for this server
and profile: steps for disabled features (requests, watch together,
recommendations, notifications) are filtered out server-side, surface=tv
drops steps needing text entry, and child profiles never see stops they
can't act on. Copy lives in Go, so a wording fix is a deploy — clients
render step kinds they know and skip unknown ones by contract.
setting_choice steps name an explicit write target (profile_field /
setting / device_setting) because playback quality is a profile column,
not a settings key — the tour writes through the same APIs the settings
screens use.
Per-profile completion state lives in the user store (SQLite schema v14
+ a Postgres twin table), keyed by (profile_id, tour_id) with monotonic
completed/skipped timestamps: finishing on one device silences every
other; a later progress write can never un-complete.
Part of #215
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(web): add the first-run feature tour
TourHost renders the server manifest as a modal overlay on Home: unknown
step kinds are skipped silently (the forward-compat contract), progress
posts per step, and setting_choice steps write real values through the
existing profile/settings mutations — by the last step the account is
genuinely configured. Skip is always one click and recorded server-side,
so no other device re-prompts. The tour ends by handing off to the
existing taste-seed picker, which now waits for the tour to finish
before its own redirect. Settings → Personalize gains a replay entry.
An invitation sent with show_tour=false plants a local hint that the
gate converts into a server-side skip for the first profile.
Part of #215
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(web): satisfy noUncheckedIndexedAccess in the tour's advance step
The Docker web build runs `tsc -b`, which applies the project's
noUncheckedIndexedAccess; the bounds check didn't narrow steps[next].
Look the step up once and branch on its presence instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(web): blur the whole app behind the tour, sidebar included
The tour overlay rendered inside the app layout, where an ancestor
creates a fixed-position containing block — inset-0 pinned to the
content pane, leaving the sidebar completely un-scrimmed. Portal the
dialog to <body> so the scrim truly covers the viewport, and raise the
backdrop blur from sm (4px) to xl (24px) so card titles and nav labels
aren't legible through it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(onboarding): name features by their UI labels in the tour copy
"Same movie, different couches" never said what the feature is called.
Every feature card now leads with the name the sidebar actually uses —
Watch Party, Requests, Watchlist, Calendar, Notifications — and says
where to find it, so the tour teaches vocabulary, not just concepts.
Server-side copy, so all three clients pick this up with no release.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(onboarding): add apps and Jellyfin-compat steps to the tour
Two new web-only feature cards near the end of the tour:
- "Take Silo with you" — native apps for iPhone/iPad/Apple TV and
Android/Android TV, with outbound TestFlight and Play Store links.
Steps gain an additive links field (label + url) that older clients
ignore; the web TourHost renders them as external-link buttons.
- "Already use a Jellyfin app? It works here" — Infuse/VidHub/Findroid/
Swiftfin connect via the Jellyfin API. Gated on
jellyfin_compat.enabled (default-on, so unset counts as enabled;
only an explicit "false" hides it).
Both steps are web-only: the apps card is pointless inside the apps it
advertises, and TV can't open store links. surface=phone/tv manifests
skip them, covered by tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(web): keep the tour card responsive on phone widths
Verified every step at 1600px, 390px, and 320px with an automated
overflow check. Fixes it found:
- Link buttons (apps step) now wrap and truncate instead of extending
past the card edge.
- The footer wraps at very narrow widths, so the handoff step's wide
primary button drops to its own line rather than overflowing.
- Progress pips hide on phones — decorative, and they crowded the
Back/Next buttons.
- The card scrolls within 85dvh so a tall step never pins its buttons
off-screen on landscape phones.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(web): render store links as branded badges in the tour
The apps step's plain outline buttons now render as store badges: the
Apple or Google Play mark with a store eyebrow (TestFlight beta /
Google Play) over the platform label — the familiar app-store badge
idiom. The brand is inferred from the link's host on the client, so
the server contract stays icon-free and non-store links keep the plain
external-link button. Labels drop the parenthesized store name the
eyebrow now carries.
Verified at 1600px and 390px with the overflow sweep: none.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
433 lines
18 KiB
Go
433 lines
18 KiB
Go
package userdb
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/Silo-Server/silo-server/internal/userstore"
|
|
)
|
|
|
|
// SQLiteUserStore implements userstore.UserStore using a per-user SQLite database.
|
|
type SQLiteUserStore struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
// NewSQLiteUserStore wraps an existing *sql.DB as a UserStore.
|
|
func NewSQLiteUserStore(db *sql.DB) *SQLiteUserStore {
|
|
return &SQLiteUserStore{db: db}
|
|
}
|
|
|
|
// Compile-time interface check.
|
|
var _ userstore.UserStore = (*SQLiteUserStore)(nil)
|
|
var _ userstore.DeviceRegistry = (*SQLiteUserStore)(nil)
|
|
|
|
// --- Profiles ---
|
|
|
|
func (s *SQLiteUserStore) CreateProfile(_ context.Context, p userstore.Profile) error {
|
|
return CreateProfile(s.db, p)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) GetProfile(_ context.Context, id string) (*userstore.Profile, error) {
|
|
return GetProfile(s.db, id)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) ListProfiles(_ context.Context) ([]userstore.Profile, error) {
|
|
return ListProfiles(s.db)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) UpdateProfile(_ context.Context, id string, u userstore.UpdateProfileInput) error {
|
|
return UpdateProfile(s.db, id, u)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) DeleteProfile(_ context.Context, id string) error {
|
|
return DeleteProfile(s.db, id)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) VerifyPIN(_ context.Context, profileID, pin string) (bool, error) {
|
|
return VerifyPIN(s.db, profileID, pin)
|
|
}
|
|
|
|
// --- Progress ---
|
|
|
|
func (s *SQLiteUserStore) UpdateProgress(_ context.Context, profileID, mediaItemID string, position, duration float64, thresholds userstore.ProgressThresholds) error {
|
|
return UpdateProgress(s.db, profileID, mediaItemID, position, duration, thresholds)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) SetProgress(_ context.Context, profileID, mediaItemID string, position, duration float64, thresholds userstore.ProgressThresholds) error {
|
|
return SetProgress(s.db, profileID, mediaItemID, position, duration, thresholds)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) SetProgressAt(_ context.Context, profileID, mediaItemID string, position, duration float64, completed bool, updatedAt time.Time) error {
|
|
return SetProgressAt(s.db, profileID, mediaItemID, position, duration, completed, updatedAt)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) SetProgressIfNewer(_ context.Context, profileID, mediaItemID string, position, duration float64, completed bool, updatedAt time.Time) (bool, error) {
|
|
return SetProgressIfNewer(s.db, profileID, mediaItemID, position, duration, completed, updatedAt)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) UpdateProgressHints(_ context.Context, profileID, mediaItemID string, hints userstore.VersionHints) error {
|
|
return UpdateProgressHints(s.db, profileID, mediaItemID, hints)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) MarkWatched(_ context.Context, profileID, mediaItemID string, duration float64) error {
|
|
return MarkWatched(s.db, profileID, mediaItemID, duration)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) ClearProgress(_ context.Context, profileID, mediaItemID string) error {
|
|
return ClearProgress(s.db, profileID, mediaItemID)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) MarkProgressBatch(_ context.Context, profileID string, mediaItemIDs []string, updatedAt time.Time) error {
|
|
return MarkProgressBatch(s.db, profileID, mediaItemIDs, updatedAt)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) ClearProgressBatch(_ context.Context, profileID string, mediaItemIDs []string, updatedAt time.Time) error {
|
|
return ClearProgressBatch(s.db, profileID, mediaItemIDs, updatedAt)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) GetProgress(_ context.Context, profileID, mediaItemID string) (*userstore.WatchProgress, error) {
|
|
return GetProgress(s.db, profileID, mediaItemID)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) ListProgress(_ context.Context, profileID, status string, limit, offset int) ([]userstore.WatchProgress, error) {
|
|
return ListProgress(s.db, profileID, status, limit, offset)
|
|
}
|
|
|
|
// ListProgressFiltered cannot push the type/library predicate down: the
|
|
// per-user SQLite store has no catalog tables (media_items/episodes/
|
|
// media_item_libraries live in the shared Postgres schema). It therefore
|
|
// returns the status page unfiltered — a valid coarse superset — and relies on
|
|
// the caller's in-memory type check and library-scoped hydration to narrow it.
|
|
func (s *SQLiteUserStore) ListProgressFiltered(_ context.Context, profileID, status string, _ []string, _ *int, limit, offset int) ([]userstore.WatchProgress, error) {
|
|
return ListProgress(s.db, profileID, status, limit, offset)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) ListProgressByMediaItems(_ context.Context, profileID string, mediaItemIDs []string) (map[string]userstore.WatchProgress, error) {
|
|
return ListProgressByMediaItems(s.db, profileID, mediaItemIDs)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) ListProgressSince(_ context.Context, profileID, cursor string) ([]userstore.WatchProgress, string, error) {
|
|
c, _ := strconv.ParseInt(cursor, 10, 64) // empty/invalid cursor → 0 (full delta)
|
|
rows, next, err := ListProgressSince(s.db, profileID, c, 0)
|
|
if err != nil {
|
|
return nil, cursor, err
|
|
}
|
|
return rows, strconv.FormatInt(next, 10), nil
|
|
}
|
|
|
|
func (s *SQLiteUserStore) AddHistory(_ context.Context, entry userstore.WatchHistoryEntry) error {
|
|
return AddHistory(s.db, entry)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) AddVisibleHistory(_ context.Context, entry userstore.WatchHistoryEntry) (userstore.WatchHistoryEntry, error) {
|
|
return AddVisibleHistory(s.db, entry)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) AddHistoryIfMissing(_ context.Context, entry userstore.WatchHistoryEntry) (bool, error) {
|
|
return AddHistoryIfMissing(s.db, entry)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) ListHistory(_ context.Context, profileID string, limit, offset int) ([]userstore.WatchHistoryEntry, error) {
|
|
return ListHistory(s.db, profileID, limit, offset)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) ListCompletedHistory(_ context.Context, query userstore.CompletedHistoryQuery) ([]userstore.WatchHistoryEntry, error) {
|
|
return ListCompletedHistory(s.db, query)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) ListCompletedHistoryItems(_ context.Context, query userstore.CompletedHistoryItemQuery) ([]userstore.CompletedHistoryItem, error) {
|
|
return ListCompletedHistoryItems(s.db, query)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) VisibleHistoryTimestamps(_ context.Context, profileID string, mediaItemIDs []string, at time.Time) (map[string]string, error) {
|
|
return VisibleHistoryTimestamps(s.db, profileID, mediaItemIDs, at)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) RemoveHistoryItems(_ context.Context, profileID string, mediaItemIDs []string, removedAt time.Time) error {
|
|
return RemoveHistoryItems(s.db, profileID, mediaItemIDs, removedAt)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) DeleteHistoryBySource(_ context.Context, profileID string, mediaItemIDs []string, source userstore.WatchHistorySource) error {
|
|
return DeleteHistoryBySource(s.db, profileID, mediaItemIDs, source)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) ListHomeDismissals(_ context.Context, profileID, surface string) ([]userstore.HomeItemDismissal, error) {
|
|
return ListHomeDismissals(s.db, profileID, surface)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) UpsertHomeDismissal(_ context.Context, dismissal userstore.HomeItemDismissal) error {
|
|
return UpsertHomeDismissal(s.db, dismissal)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) DeleteHomeDismissal(_ context.Context, profileID, surface, mediaItemID string) error {
|
|
return DeleteHomeDismissal(s.db, profileID, surface, mediaItemID)
|
|
}
|
|
|
|
// --- Favorites & Watchlist ---
|
|
|
|
func (s *SQLiteUserStore) AddFavorite(_ context.Context, profileID, mediaItemID string) error {
|
|
return AddFavorite(s.db, profileID, mediaItemID)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) AddFavoriteAt(_ context.Context, profileID, mediaItemID string, addedAt time.Time) (bool, error) {
|
|
return AddFavoriteAt(s.db, profileID, mediaItemID, addedAt)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) RemoveFavorite(_ context.Context, profileID, mediaItemID string) error {
|
|
return RemoveFavorite(s.db, profileID, mediaItemID)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) ListFavorites(_ context.Context, profileID string, limit, offset int) ([]userstore.Favorite, error) {
|
|
return ListFavorites(s.db, profileID, limit, offset)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) ListFavoritesByMediaItems(_ context.Context, profileID string, mediaItemIDs []string) (map[string]bool, error) {
|
|
return ListFavoritesByMediaItems(s.db, profileID, mediaItemIDs)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) IsFavorite(_ context.Context, profileID, mediaItemID string) (bool, error) {
|
|
return IsFavorite(s.db, profileID, mediaItemID)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) AddToWatchlist(_ context.Context, profileID, mediaItemID string) error {
|
|
return AddToWatchlist(s.db, profileID, mediaItemID)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) AddToWatchlistAt(_ context.Context, profileID, mediaItemID string, addedAt time.Time) (bool, error) {
|
|
return AddToWatchlistAt(s.db, profileID, mediaItemID, addedAt)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) RemoveFromWatchlist(_ context.Context, profileID, mediaItemID string) error {
|
|
return RemoveFromWatchlist(s.db, profileID, mediaItemID)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) ReplaceWatchlistOrder(_ context.Context, profileID string, orderedMediaItemIDs []string) error {
|
|
return ReplaceWatchlistOrder(s.db, profileID, orderedMediaItemIDs)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) ListWatchlist(_ context.Context, profileID string, limit, offset int) ([]userstore.WatchlistEntry, error) {
|
|
return ListWatchlist(s.db, profileID, limit, offset)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) ListWatchlistByMediaItems(_ context.Context, profileID string, mediaItemIDs []string) (map[string]bool, error) {
|
|
return ListWatchlistByMediaItems(s.db, profileID, mediaItemIDs)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) InWatchlist(_ context.Context, profileID, mediaItemID string) (bool, error) {
|
|
return InWatchlist(s.db, profileID, mediaItemID)
|
|
}
|
|
|
|
// RemoveWatchedFromWatchlist defaults on for the embedded sqlite backend, which
|
|
// does not persist the per-profile preference.
|
|
func (s *SQLiteUserStore) RemoveWatchedFromWatchlist(_ context.Context, _ string) (bool, error) {
|
|
return true, nil
|
|
}
|
|
|
|
// --- Collections ---
|
|
|
|
func (s *SQLiteUserStore) CreateCollection(_ context.Context, input userstore.CreateCollectionInput) (*userstore.Collection, error) {
|
|
return CreateCollection(s.db, input)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) GetCollection(_ context.Context, id string) (*userstore.Collection, error) {
|
|
return GetCollection(s.db, id)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) ListCollections(_ context.Context, profileID string) ([]userstore.Collection, error) {
|
|
return ListCollections(s.db, profileID)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) UpdateCollection(_ context.Context, input userstore.UpdateCollectionInput) error {
|
|
return UpdateCollection(s.db, input)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) DeleteCollection(_ context.Context, id string) error {
|
|
return DeleteCollection(s.db, id)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) AddCollectionItem(_ context.Context, collectionID, mediaItemID string, position int) error {
|
|
return AddCollectionItem(s.db, collectionID, mediaItemID, position)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) RemoveCollectionItem(_ context.Context, collectionID, mediaItemID string) error {
|
|
return RemoveCollectionItem(s.db, collectionID, mediaItemID)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) ListCollectionItems(_ context.Context, collectionID string) ([]userstore.CollectionItem, error) {
|
|
return ListCollectionItems(s.db, collectionID)
|
|
}
|
|
|
|
// User-collection imports are Postgres-only; SQLite never receives sync writes.
|
|
func (s *SQLiteUserStore) ReplaceCollectionItems(_ context.Context, _ string, _ []userstore.CollectionItemReplacement) error {
|
|
return fmt.Errorf("user collection imports are not supported on the SQLite user store")
|
|
}
|
|
|
|
func (s *SQLiteUserStore) ReorderCollectionItems(_ context.Context, _ string, _ []string) error {
|
|
return fmt.Errorf("collection reordering is not supported on the SQLite user store")
|
|
}
|
|
|
|
func (s *SQLiteUserStore) ReorderCollections(_ context.Context, _ string, _ *string, _ []string) error {
|
|
return fmt.Errorf("collection reordering is not supported on the SQLite user store")
|
|
}
|
|
|
|
func (s *SQLiteUserStore) UpdateCollectionSyncState(_ context.Context, _ userstore.UpdateCollectionSyncStateInput) error {
|
|
return fmt.Errorf("user collection imports are not supported on the SQLite user store")
|
|
}
|
|
|
|
func (s *SQLiteUserStore) ListCollectionGroups(_ context.Context) ([]userstore.CollectionGroup, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (s *SQLiteUserStore) EnsureCollectionGroup(_ context.Context, _ string) error {
|
|
return nil
|
|
}
|
|
|
|
func (s *SQLiteUserStore) CreateCollectionGroup(_ context.Context, _, _ string, _ userstore.GroupSortMode) (*userstore.CollectionGroup, error) {
|
|
return nil, fmt.Errorf("collection groups are not supported on the SQLite user store")
|
|
}
|
|
|
|
func (s *SQLiteUserStore) UpdateCollectionGroup(_ context.Context, _ string, _ *string, _ *string, _ *userstore.GroupSortMode) (*userstore.CollectionGroup, error) {
|
|
return nil, fmt.Errorf("collection groups are not supported on the SQLite user store")
|
|
}
|
|
|
|
func (s *SQLiteUserStore) DeleteCollectionGroup(_ context.Context, _ string) error {
|
|
return fmt.Errorf("collection groups are not supported on the SQLite user store")
|
|
}
|
|
|
|
func (s *SQLiteUserStore) ReorderCollectionGroups(_ context.Context, _ []string) error {
|
|
return fmt.Errorf("collection groups are not supported on the SQLite user store")
|
|
}
|
|
|
|
// --- Section Overrides ---
|
|
|
|
func (s *SQLiteUserStore) ListSectionOverrides(_ context.Context, profileID, scope, libraryID string) ([]userstore.SectionOverride, error) {
|
|
return ListSectionOverrides(s.db, profileID, scope, libraryID)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) SaveSectionOverrides(_ context.Context, profileID, scope, libraryID string, overrides []userstore.SectionOverride) error {
|
|
return SaveSectionOverrides(s.db, profileID, scope, libraryID, overrides)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) ResetSectionOverrides(_ context.Context, profileID, scope, libraryID string) error {
|
|
return ResetSectionOverrides(s.db, profileID, scope, libraryID)
|
|
}
|
|
|
|
// --- Settings & Preferences ---
|
|
|
|
func (s *SQLiteUserStore) GetSetting(_ context.Context, key string) (string, error) {
|
|
return GetSetting(s.db, key)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) SetSetting(_ context.Context, key, value string) error {
|
|
return SetSetting(s.db, key, value)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) DeleteSetting(_ context.Context, key string) error {
|
|
return DeleteSetting(s.db, key)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) ListSettings(_ context.Context) ([]userstore.SettingEntry, error) {
|
|
return ListSettings(s.db)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) GetDeviceSetting(_ context.Context, profileID, deviceID, key string) (*userstore.DeviceSettingEntry, error) {
|
|
return GetDeviceSetting(s.db, profileID, deviceID, key)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) RegisterDevice(_ context.Context, entry userstore.DeviceEntry) error {
|
|
return RegisterDevice(s.db, entry)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) ListDevices(_ context.Context) ([]userstore.DeviceEntry, error) {
|
|
return ListDevices(s.db)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) SetDeviceSetting(_ context.Context, entry userstore.DeviceSettingEntry) error {
|
|
return SetDeviceSetting(s.db, entry)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) DeleteDeviceSetting(_ context.Context, profileID, deviceID, key string) error {
|
|
return DeleteDeviceSetting(s.db, profileID, deviceID, key)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) DeleteAllDeviceSettings(_ context.Context, profileID, deviceID string) error {
|
|
return DeleteAllDeviceSettings(s.db, profileID, deviceID)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) DeleteDeviceSettingsByKey(_ context.Context, key string) error {
|
|
return DeleteDeviceSettingsByKey(s.db, key)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) ListDeviceSettings(_ context.Context, key string) ([]userstore.DeviceSettingEntry, error) {
|
|
return ListDeviceSettings(s.db, key)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) ListAllDeviceSettings(_ context.Context) ([]userstore.DeviceSettingEntry, error) {
|
|
return ListAllDeviceSettings(s.db)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) SetSubtitlePreference(_ context.Context, pref userstore.SubtitlePreference) error {
|
|
return SetSubtitlePreference(s.db, pref)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) GetSubtitlePreference(_ context.Context, profileID, seriesID string) (*userstore.SubtitlePreference, error) {
|
|
return GetSubtitlePreference(s.db, profileID, seriesID)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) DeleteSubtitlePreference(_ context.Context, profileID, seriesID string) error {
|
|
return DeleteSubtitlePreference(s.db, profileID, seriesID)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) SetAudioPreference(_ context.Context, pref userstore.AudioPreference) error {
|
|
return SetAudioPreference(s.db, pref)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) GetAudioPreference(_ context.Context, profileID, seriesID string) (*userstore.AudioPreference, error) {
|
|
return GetAudioPreference(s.db, profileID, seriesID)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) DeleteAudioPreference(_ context.Context, profileID, seriesID string) error {
|
|
return DeleteAudioPreference(s.db, profileID, seriesID)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) SetSeriesPlaybackPreference(_ context.Context, pref userstore.SeriesPlaybackPreference) error {
|
|
return SetSeriesPlaybackPreference(s.db, pref)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) GetSeriesPlaybackPreference(_ context.Context, profileID, seriesID string) (*userstore.SeriesPlaybackPreference, error) {
|
|
return GetSeriesPlaybackPreference(s.db, profileID, seriesID)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) DeleteSeriesPlaybackPreference(_ context.Context, profileID, seriesID string) error {
|
|
return DeleteSeriesPlaybackPreference(s.db, profileID, seriesID)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) GetLibraryPlaybackPreference(_ context.Context, profileID string, libraryID int) (*userstore.LibraryPlaybackPreference, error) {
|
|
return GetLibraryPlaybackPreference(s.db, profileID, libraryID)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) ListLibraryPlaybackPreferences(_ context.Context, profileID string) ([]userstore.LibraryPlaybackPreference, error) {
|
|
return ListLibraryPlaybackPreferences(s.db, profileID)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) UpsertLibraryPlaybackPreference(_ context.Context, pref userstore.LibraryPlaybackPreference) error {
|
|
return UpsertLibraryPlaybackPreference(s.db, pref)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) DeleteLibraryPlaybackPreference(_ context.Context, profileID string, libraryID int) error {
|
|
return DeleteLibraryPlaybackPreference(s.db, profileID, libraryID)
|
|
}
|
|
|
|
// --- Onboarding ---
|
|
|
|
func (s *SQLiteUserStore) GetOnboardingState(_ context.Context, profileID, tourID string) (*userstore.OnboardingState, error) {
|
|
return GetOnboardingState(s.db, profileID, tourID)
|
|
}
|
|
|
|
func (s *SQLiteUserStore) UpsertOnboardingState(_ context.Context, state userstore.OnboardingState) error {
|
|
return UpsertOnboardingState(s.db, state)
|
|
}
|