Files
54507d236a fix(watchstate): make series mark-watched atomic and unload-proof (#645)
Marking a large series as watched only marked some episodes when the user
navigated away or closed the tab. POST /watched/{id} expanded a series to
every episode and then did four sequential DB round-trips per episode — a
duration lookup, a progress upsert, a stable-identity resolution (itself an
episode lookup plus a series provider-ID lookup), and a history insert — all
on the request context with no transaction. A 200-episode series was ~800
sequential queries, each committing on its own, so a disconnect mid-loop left
everything already committed in place.

Server:
- episodeTargets now resolves durations through the existing batched
  listEpisodeFiles helper instead of one file query per episode. The shared
  mediaFileDurationSeconds keeps the batched and single-item paths in step.
- ResolveHistoryIdentities resolves a whole series in one episode query plus
  one provider-ID query per distinct series, behind optional-capability
  interfaces with per-ID fallbacks.
- New userstore.WatchedBatchWriter capability, implemented transactionally in
  both Postgres and SQLite, with a per-target fallback for stores that lack
  it. recordMarkWatched and jellycompat's recordMarkWatchedBatch share it.

A cancelled request now rolls back to "nothing marked" rather than stranding
a half-watched series, which is the correct all-or-nothing semantic and needs
no detached context.

Client: the watched mutation sends keepalive so the request survives
navigation and tab close, plus an optimistic played flip with rollback,
following the existing favorites.ts pattern.

Routing jellycompat through the shared path would have clobbered known
durations with 0, since that caller supplies none; the batch upsert only ever
advances a duration, matching what MarkProgressBatch did before.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 23:15:56 -04:00

523 lines
22 KiB
Go

package userdb
import (
"context"
"database/sql"
"encoding/json"
"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)
var _ userstore.WatchedBatchWriter = (*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)
}
// MarkWatchedBatch is one of the few paths here that forwards ctx: the write
// is transactional precisely so a canceled request marks nothing.
func (s *SQLiteUserStore) MarkWatchedBatch(ctx context.Context, profileID string, targets []userstore.MarkWatchedTarget, entries []userstore.WatchHistoryEntry) ([]userstore.WatchHistoryEntry, error) {
return MarkWatchedBatch(ctx, s.db, profileID, targets, entries)
}
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) DeviceExists(_ context.Context, profileID, deviceID string) (bool, error) {
return DeviceExists(s.db, profileID, deviceID)
}
func (s *SQLiteUserStore) ForgetDevice(_ context.Context, profileID, deviceID string) error {
return ForgetDevice(s.db, profileID, deviceID)
}
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) GetJellycompatDisplayPrefs(_ context.Context, prefsID, client string) (string, error) {
return GetJellycompatDisplayPrefs(s.db, prefsID, client)
}
func (s *SQLiteUserStore) SetJellycompatDisplayPrefs(_ context.Context, prefsID, client, value string) error {
return SetJellycompatDisplayPrefs(s.db, prefsID, client, value)
}
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) SetCollectionSortPreference(_ context.Context, pref userstore.CollectionSortPreference) error {
return SetCollectionSortPreference(s.db, pref)
}
func (s *SQLiteUserStore) GetCollectionSortPreference(_ context.Context, profileID, collectionKind, collectionID string) (*userstore.CollectionSortPreference, error) {
return GetCollectionSortPreference(s.db, profileID, collectionKind, collectionID)
}
func (s *SQLiteUserStore) ClearCollectionSortPreference(_ context.Context, profileID, collectionKind, collectionID string) error {
return ClearCollectionSortPreference(s.db, profileID, collectionKind, collectionID)
}
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)
}
// --- Canonical typed setting values ---
func (s *SQLiteUserStore) GetSettingValue(_ context.Context, id userstore.SettingIdentity) (*userstore.SettingValue, error) {
return GetSettingValue(s.db, id)
}
func (s *SQLiteUserStore) ListSettingValuesForResolution(_ context.Context, query userstore.SettingResolutionQuery) ([]userstore.SettingValue, error) {
return ListSettingValuesForResolution(s.db, query)
}
func (s *SQLiteUserStore) ListAllSettingValues(_ context.Context) ([]userstore.SettingValue, error) {
return ListAllSettingValues(s.db)
}
func (s *SQLiteUserStore) UpsertSettingValue(_ context.Context, id userstore.SettingIdentity, value json.RawMessage) (*userstore.SettingValue, error) {
return UpsertSettingValue(s.db, id, value)
}
func (s *SQLiteUserStore) CompareAndSetSettingValue(ctx context.Context, id userstore.SettingIdentity, value json.RawMessage, expectedRevision int64) (*userstore.SettingValue, error) {
return CompareAndSetSettingValue(ctx, s.db, id, value, expectedRevision)
}
func (s *SQLiteUserStore) DeleteSettingValue(_ context.Context, id userstore.SettingIdentity) (bool, error) {
return DeleteSettingValue(s.db, id)
}
func (s *SQLiteUserStore) DeleteSettingValuesForProfile(_ context.Context, profileID string) (int64, error) {
return DeleteSettingValuesForProfile(s.db, profileID)
}
func (s *SQLiteUserStore) DeleteSettingValuesForDevice(_ context.Context, profileID, deviceID string) (int64, error) {
return DeleteSettingValuesForDevice(s.db, profileID, deviceID)
}
func (s *SQLiteUserStore) DeleteSettingValuesForLibrary(_ context.Context, libraryID int) (int64, error) {
return DeleteSettingValuesForLibrary(s.db, libraryID)
}
func (s *SQLiteUserStore) DeleteSettingValuesForSeries(_ context.Context, seriesID string) (int64, error) {
return DeleteSettingValuesForSeries(s.db, seriesID)
}
func (s *SQLiteUserStore) GetSettingMutation(_ context.Context, mutationID string) (*userstore.SettingMutationRecord, error) {
return GetSettingMutation(s.db, mutationID)
}
func (s *SQLiteUserStore) PutSettingMutation(_ context.Context, record userstore.SettingMutationRecord) (userstore.SettingMutationRecord, bool, error) {
return PutSettingMutation(s.db, record)
}
func (s *SQLiteUserStore) DeleteExpiredSettingMutations(_ context.Context, before time.Time) (int64, error) {
return DeleteExpiredSettingMutations(s.db, before)
}