* feat(metadata): add original-language preferences * fix(settings): show metadata exceptions immediately * docs(settings): add metadata language screenshot * fix(settings): make language exceptions responsive * fix(settings): standardize language display names
146 lines
4.8 KiB
Go
146 lines
4.8 KiB
Go
package access
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"log/slog"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/Silo-Server/silo-server/internal/lang"
|
|
"github.com/Silo-Server/silo-server/internal/settingscontract"
|
|
"github.com/Silo-Server/silo-server/internal/settingskeys"
|
|
"github.com/Silo-Server/silo-server/internal/settingsresolve"
|
|
"github.com/Silo-Server/silo-server/internal/userstore"
|
|
)
|
|
|
|
// ViewerPreferences are the canonical preferences needed while constructing
|
|
// an access scope. They are resolved together because this path runs on nearly
|
|
// every authenticated request and one candidate read can answer both keys.
|
|
type ViewerPreferences struct {
|
|
DisabledLibraryIDs []int
|
|
PreferredMetadataLanguage string
|
|
MetadataLanguageOverrides map[string]string
|
|
}
|
|
|
|
// ResolveViewerPreferences resolves the profile's viewer-scope preferences in
|
|
// one canonical store read. The legacy disabled_library_ids account setting is
|
|
// consulted only when no canonical row decided that value.
|
|
func ResolveViewerPreferences(
|
|
ctx context.Context, store userstore.UserStore, profileID string,
|
|
) ViewerPreferences {
|
|
profileID = strings.TrimSpace(profileID)
|
|
if store == nil {
|
|
return ViewerPreferences{}
|
|
}
|
|
if profileID == "" {
|
|
return ViewerPreferences{DisabledLibraryIDs: legacyDisabledLibraryIDs(ctx, store)}
|
|
}
|
|
|
|
resolved, ok := resolveCanonicalViewerPreferences(ctx, store, profileID)
|
|
if !ok || !resolved.disabledLibraryIDsSet {
|
|
resolved.preferences.DisabledLibraryIDs = legacyDisabledLibraryIDs(ctx, store)
|
|
}
|
|
return resolved.preferences
|
|
}
|
|
|
|
type canonicalViewerPreferences struct {
|
|
preferences ViewerPreferences
|
|
disabledLibraryIDsSet bool
|
|
}
|
|
|
|
func resolveCanonicalViewerPreferences(
|
|
ctx context.Context, store userstore.UserStore, profileID string,
|
|
) (canonicalViewerPreferences, bool) {
|
|
contract, err := settingscontract.Load()
|
|
if err != nil {
|
|
slog.WarnContext(ctx, "viewer preference resolution degraded: loading settings contract failed",
|
|
"component", "access", "profile_id", profileID, "error", err)
|
|
return canonicalViewerPreferences{}, false
|
|
}
|
|
values, err := settingsresolve.New(contract).Resolve(ctx, store,
|
|
settingsresolve.Context{ProfileID: profileID},
|
|
[]string{
|
|
settingskeys.UiDisabledLibraryIds,
|
|
settingskeys.CatalogMetadataLanguage,
|
|
settingskeys.CatalogMetadataLanguageOverrides,
|
|
}, nil)
|
|
if err != nil {
|
|
slog.WarnContext(ctx, "viewer preference resolution degraded: reading setting values failed",
|
|
"component", "access", "profile_id", profileID, "error", err)
|
|
return canonicalViewerPreferences{}, false
|
|
}
|
|
|
|
var out canonicalViewerPreferences
|
|
for _, value := range values {
|
|
switch value.Key {
|
|
case settingskeys.UiDisabledLibraryIds:
|
|
out.disabledLibraryIDsSet = value.Source != settingscontract.ScopeDefault
|
|
if out.disabledLibraryIDsSet {
|
|
out.preferences.DisabledLibraryIDs = parseLibraryIDList(value.Value)
|
|
}
|
|
case settingskeys.CatalogMetadataLanguage:
|
|
var language string
|
|
if json.Unmarshal(value.Value, &language) == nil {
|
|
out.preferences.PreferredMetadataLanguage = strings.TrimSpace(language)
|
|
}
|
|
case settingskeys.CatalogMetadataLanguageOverrides:
|
|
out.preferences.MetadataLanguageOverrides = parseMetadataLanguageOverrides(value.Value)
|
|
}
|
|
}
|
|
return out, true
|
|
}
|
|
|
|
// OriginalMetadataLanguage is a private-use BCP 47 tag stored in
|
|
// catalog.metadata_language (or as an override target) to mean "resolve this
|
|
// media item's original language." Keeping the sentinel a valid language tag
|
|
// preserves the existing setting's wire type.
|
|
const OriginalMetadataLanguage = "x-silo-original"
|
|
|
|
// parseMetadataLanguageOverrides normalizes source aliases and target tag
|
|
// casing before the map reaches catalog serving. The contract only accepts
|
|
// canonical source keys from normal clients, but normalization also makes old
|
|
// or manually-authored rows deterministic.
|
|
func parseMetadataLanguageOverrides(raw json.RawMessage) map[string]string {
|
|
var stored map[string]string
|
|
if json.Unmarshal(raw, &stored) != nil || len(stored) == 0 {
|
|
return nil
|
|
}
|
|
|
|
keys := make([]string, 0, len(stored))
|
|
for source := range stored {
|
|
keys = append(keys, source)
|
|
}
|
|
sort.Strings(keys)
|
|
|
|
out := make(map[string]string, len(stored))
|
|
for _, source := range keys {
|
|
canonicalSource := lang.Canonical(source)
|
|
if canonicalSource == "" {
|
|
continue
|
|
}
|
|
// When aliases collapse (for example en and eng), the canonical key
|
|
// wins because it sorts first and is never overwritten.
|
|
if _, exists := out[canonicalSource]; exists {
|
|
continue
|
|
}
|
|
target, ok := settingscontract.NormalizeLanguageTag(stored[source])
|
|
if !ok {
|
|
continue
|
|
}
|
|
out[canonicalSource] = target
|
|
}
|
|
if len(out) == 0 {
|
|
return nil
|
|
}
|
|
return out
|
|
}
|
|
|
|
func legacyDisabledLibraryIDs(ctx context.Context, store userstore.UserStore) []int {
|
|
raw, err := store.GetSetting(ctx, settingKeyDisabledLibraryIDs)
|
|
if err != nil || raw == "" {
|
|
return nil
|
|
}
|
|
return parseLibraryIDList(json.RawMessage(raw))
|
|
}
|