The Jellyfin DisplayPreferences blobs rode the legacy user_settings key/value table under synthetic jellycompat:* keys, which forced the legacy settings API to carry a prefix carve-out in its otherwise-closed unknown-key gate. They are the compat subsystem's storage, not user settings: the contract neither validates nor resolves them. Move them to a dedicated jellycompat_displayprefs table in both backends, keyed by (prefs id, client) per user, with the blob stored as opaque text served back byte-for-byte (deliberately not jsonb, which would re-serialize it). The data-copy migrations — per-user SQLite V16 and a paired SQL + Go goose migration for Postgres — are transactional and harmless to re-run, and both drive their key parsing and row classification from the new internal/jellycompat/displayprefs package so the backends cannot diverge, following the internal/settingsmigrate precedent. A jellycompat:* row that does not parse as a DisplayPrefs key (only ever writable through the removed carve-out) is recorded in user_setting_migration_rejects rather than silently deleted. With the last non-settings tenant gone, the jellycompatSettingPrefix carve-out is deleted: the legacy settings endpoints now refuse jellycompat:* keys like any other unknown key and never surface them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
43 lines
1.2 KiB
Go
43 lines
1.2 KiB
Go
package userdb
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// GetJellycompatDisplayPrefs returns the stored DisplayPreferences blob for
|
|
// one (prefs id, client), or "" when nothing is stored.
|
|
func GetJellycompatDisplayPrefs(db *sql.DB, prefsID, client string) (string, error) {
|
|
var value string
|
|
err := db.QueryRow(
|
|
"SELECT value FROM jellycompat_displayprefs WHERE prefs_id = ? AND client = ?",
|
|
prefsID, client,
|
|
).Scan(&value)
|
|
if err == sql.ErrNoRows {
|
|
return "", nil
|
|
}
|
|
if err != nil {
|
|
return "", fmt.Errorf("getting display prefs %q/%q: %w", prefsID, client, err)
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
// SetJellycompatDisplayPrefs stores a DisplayPreferences blob verbatim,
|
|
// replacing any previous value for the same (prefs id, client).
|
|
func SetJellycompatDisplayPrefs(db *sql.DB, prefsID, client, value string) error {
|
|
updatedAt := time.Now().UTC().Format(time.RFC3339Nano)
|
|
_, err := db.Exec(
|
|
`INSERT INTO jellycompat_displayprefs (prefs_id, client, value, updated_at)
|
|
VALUES (?, ?, ?, ?)
|
|
ON CONFLICT(prefs_id, client) DO UPDATE SET
|
|
value = excluded.value,
|
|
updated_at = excluded.updated_at`,
|
|
prefsID, client, value, updatedAt,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("setting display prefs %q/%q: %w", prefsID, client, err)
|
|
}
|
|
return nil
|
|
}
|