mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-11-15 23:00:10 +00:00
Harvey Tindall
0330540f87
The local app translations are loaded, and then if [files]/lang_files is provided (a directory containing custom translations), any found inside it are loaded over top. This makes customizing much easier.
546 lines
14 KiB
Go
546 lines
14 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io/fs"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type Storage struct {
|
|
timePattern string
|
|
invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path string
|
|
invites Invites
|
|
profiles map[string]Profile
|
|
defaultProfile string
|
|
emails, policy, configuration, displayprefs, ombi_template map[string]interface{}
|
|
lang Lang
|
|
}
|
|
|
|
// timePattern: %Y-%m-%dT%H:%M:%S.%f
|
|
|
|
type Profile struct {
|
|
Admin bool `json:"admin,omitempty"`
|
|
LibraryAccess string `json:"libraries,omitempty"`
|
|
FromUser string `json:"fromUser,omitempty"`
|
|
Policy map[string]interface{} `json:"policy,omitempty"`
|
|
Configuration map[string]interface{} `json:"configuration,omitempty"`
|
|
Displayprefs map[string]interface{} `json:"displayprefs,omitempty"`
|
|
Default bool `json:"default,omitempty"`
|
|
}
|
|
|
|
type Invite struct {
|
|
Created time.Time `json:"created"`
|
|
NoLimit bool `json:"no-limit"`
|
|
RemainingUses int `json:"remaining-uses"`
|
|
ValidTill time.Time `json:"valid_till"`
|
|
Email string `json:"email"`
|
|
UsedBy [][]string `json:"used-by"`
|
|
Notify map[string]map[string]bool `json:"notify"`
|
|
Profile string `json:"profile"`
|
|
Label string `json:"label,omitempty"`
|
|
Keys []string `json"keys,omitempty"`
|
|
}
|
|
|
|
type Lang struct {
|
|
chosenFormLang string
|
|
chosenAdminLang string
|
|
chosenEmailLang string
|
|
AdminPath string
|
|
Admin adminLangs
|
|
AdminJSON map[string]string
|
|
FormPath string
|
|
Form formLangs
|
|
EmailPath string
|
|
Email emailLangs
|
|
CommonPath string
|
|
Common commonLangs
|
|
SetupPath string
|
|
Setup setupLangs
|
|
}
|
|
|
|
func (st *Storage) loadLang(filesystems ...fs.FS) (err error) {
|
|
err = st.loadLangCommon(filesystems...)
|
|
if err != nil {
|
|
return
|
|
}
|
|
err = st.loadLangAdmin(filesystems...)
|
|
if err != nil {
|
|
return
|
|
}
|
|
err = st.loadLangForm(filesystems...)
|
|
if err != nil {
|
|
return
|
|
}
|
|
err = st.loadLangEmail(filesystems...)
|
|
return
|
|
}
|
|
|
|
func (common *commonLangs) patchCommon(lang string, other *langSection) {
|
|
if *other == nil {
|
|
*other = langSection{}
|
|
}
|
|
if _, ok := (*common)[lang]; !ok {
|
|
lang = "en-us"
|
|
}
|
|
for n, ev := range (*common)[lang].Strings {
|
|
if v, ok := (*other)[n]; !ok || v == "" {
|
|
(*other)[n] = ev
|
|
}
|
|
}
|
|
}
|
|
|
|
// If a given language has missing values, fill it in with the english value.
|
|
func patchLang(english, other *langSection) {
|
|
if *other == nil {
|
|
*other = langSection{}
|
|
}
|
|
for n, ev := range *english {
|
|
if v, ok := (*other)[n]; !ok || v == "" {
|
|
(*other)[n] = ev
|
|
}
|
|
}
|
|
}
|
|
|
|
func patchQuantityStrings(english, other *map[string]quantityString) {
|
|
for n, ev := range *english {
|
|
qs, ok := (*other)[n]
|
|
if !ok {
|
|
(*other)[n] = ev
|
|
return
|
|
} else if qs.Singular == "" {
|
|
qs.Singular = ev.Singular
|
|
} else if (*other)[n].Plural == "" {
|
|
qs.Plural = ev.Plural
|
|
}
|
|
(*other)[n] = qs
|
|
}
|
|
}
|
|
|
|
func (st *Storage) loadLangCommon(filesystems ...fs.FS) error {
|
|
st.lang.Common = map[string]commonLang{}
|
|
var english commonLang
|
|
load := func(filesystem fs.FS, fname string) error {
|
|
index := strings.TrimSuffix(fname, filepath.Ext(fname))
|
|
lang := commonLang{}
|
|
f, err := fs.ReadFile(filesystem, filepath.Join(st.lang.CommonPath, fname))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if substituteStrings != "" {
|
|
f = []byte(strings.ReplaceAll(string(f), "Jellyfin", substituteStrings))
|
|
}
|
|
err = json.Unmarshal(f, &lang)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if fname != "en-us.json" {
|
|
patchLang(&english.Strings, &lang.Strings)
|
|
}
|
|
st.lang.Common[index] = lang
|
|
return nil
|
|
}
|
|
engFound := false
|
|
var err error
|
|
for _, filesystem := range filesystems {
|
|
err = load(filesystem, "en-us.json")
|
|
if err == nil {
|
|
engFound = true
|
|
}
|
|
}
|
|
if !engFound {
|
|
return err
|
|
}
|
|
english = st.lang.Common["en-us"]
|
|
commonLoaded := false
|
|
for _, filesystem := range filesystems {
|
|
files, err := fs.ReadDir(filesystem, st.lang.CommonPath)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, f := range files {
|
|
if f.Name() != "en-us.json" {
|
|
err = load(filesystem, f.Name())
|
|
if err == nil {
|
|
commonLoaded = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if !commonLoaded {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (st *Storage) loadLangAdmin(filesystems ...fs.FS) error {
|
|
st.lang.Admin = map[string]adminLang{}
|
|
var english adminLang
|
|
load := func(filesystem fs.FS, fname string) error {
|
|
index := strings.TrimSuffix(fname, filepath.Ext(fname))
|
|
lang := adminLang{}
|
|
f, err := fs.ReadFile(filesystem, filepath.Join(st.lang.AdminPath, fname))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if substituteStrings != "" {
|
|
f = []byte(strings.ReplaceAll(string(f), "Jellyfin", substituteStrings))
|
|
}
|
|
err = json.Unmarshal(f, &lang)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
st.lang.Common.patchCommon(index, &lang.Strings)
|
|
if fname != "en-us.json" {
|
|
patchLang(&english.Strings, &lang.Strings)
|
|
patchLang(&english.Notifications, &lang.Notifications)
|
|
patchQuantityStrings(&english.QuantityStrings, &lang.QuantityStrings)
|
|
}
|
|
stringAdmin, err := json.Marshal(lang)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
lang.JSON = string(stringAdmin)
|
|
st.lang.Admin[index] = lang
|
|
return nil
|
|
}
|
|
engFound := false
|
|
var err error
|
|
for _, filesystem := range filesystems {
|
|
err = load(filesystem, "en-us.json")
|
|
if err == nil {
|
|
engFound = true
|
|
}
|
|
}
|
|
if !engFound {
|
|
return err
|
|
}
|
|
english = st.lang.Admin["en-us"]
|
|
adminLoaded := false
|
|
for _, filesystem := range filesystems {
|
|
files, err := fs.ReadDir(filesystem, st.lang.AdminPath)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, f := range files {
|
|
if f.Name() != "en-us.json" {
|
|
err = load(filesystem, f.Name())
|
|
if err == nil {
|
|
adminLoaded = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if !adminLoaded {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (st *Storage) loadLangForm(filesystems ...fs.FS) error {
|
|
st.lang.Form = map[string]formLang{}
|
|
var english formLang
|
|
load := func(filesystem fs.FS, fname string) error {
|
|
index := strings.TrimSuffix(fname, filepath.Ext(fname))
|
|
lang := formLang{}
|
|
f, err := fs.ReadFile(filesystem, filepath.Join(st.lang.FormPath, fname))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if substituteStrings != "" {
|
|
f = []byte(strings.ReplaceAll(string(f), "Jellyfin", substituteStrings))
|
|
}
|
|
err = json.Unmarshal(f, &lang)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
st.lang.Common.patchCommon(index, &lang.Strings)
|
|
if fname != "en-us.json" {
|
|
patchLang(&english.Strings, &lang.Strings)
|
|
patchLang(&english.Notifications, &lang.Notifications)
|
|
patchQuantityStrings(&english.ValidationStrings, &lang.ValidationStrings)
|
|
}
|
|
notifications, err := json.Marshal(lang.Notifications)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
validationStrings, err := json.Marshal(lang.ValidationStrings)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
lang.notificationsJSON = string(notifications)
|
|
lang.validationStringsJSON = string(validationStrings)
|
|
st.lang.Form[index] = lang
|
|
return nil
|
|
}
|
|
engFound := false
|
|
var err error
|
|
for _, filesystem := range filesystems {
|
|
err = load(filesystem, "en-us.json")
|
|
if err == nil {
|
|
engFound = true
|
|
}
|
|
}
|
|
if !engFound {
|
|
return err
|
|
}
|
|
english = st.lang.Form["en-us"]
|
|
formLoaded := false
|
|
for _, filesystem := range filesystems {
|
|
files, err := fs.ReadDir(filesystem, st.lang.FormPath)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, f := range files {
|
|
if f.Name() != "en-us.json" {
|
|
err = load(filesystem, f.Name())
|
|
if err == nil {
|
|
formLoaded = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if !formLoaded {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (st *Storage) loadLangEmail(filesystems ...fs.FS) error {
|
|
st.lang.Email = map[string]emailLang{}
|
|
var english emailLang
|
|
load := func(filesystem fs.FS, fname string) error {
|
|
index := strings.TrimSuffix(fname, filepath.Ext(fname))
|
|
lang := emailLang{}
|
|
f, err := fs.ReadFile(filesystem, filepath.Join(st.lang.EmailPath, fname))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if substituteStrings != "" {
|
|
f = []byte(strings.ReplaceAll(string(f), "Jellyfin", substituteStrings))
|
|
}
|
|
err = json.Unmarshal(f, &lang)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
st.lang.Common.patchCommon(index, &lang.Strings)
|
|
if fname != "en-us.json" {
|
|
patchLang(&english.UserCreated, &lang.UserCreated)
|
|
patchLang(&english.InviteExpiry, &lang.InviteExpiry)
|
|
patchLang(&english.PasswordReset, &lang.PasswordReset)
|
|
patchLang(&english.UserDeleted, &lang.UserDeleted)
|
|
patchLang(&english.InviteEmail, &lang.InviteEmail)
|
|
patchLang(&english.WelcomeEmail, &lang.WelcomeEmail)
|
|
}
|
|
st.lang.Email[index] = lang
|
|
return nil
|
|
}
|
|
engFound := false
|
|
var err error
|
|
for _, filesystem := range filesystems {
|
|
err = load(filesystem, "en-us.json")
|
|
if err == nil {
|
|
engFound = true
|
|
}
|
|
}
|
|
if !engFound {
|
|
return err
|
|
}
|
|
english = st.lang.Email["en-us"]
|
|
emailLoaded := false
|
|
for _, filesystem := range filesystems {
|
|
files, err := fs.ReadDir(filesystem, st.lang.EmailPath)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, f := range files {
|
|
if f.Name() != "en-us.json" {
|
|
err = load(filesystem, f.Name())
|
|
if err == nil {
|
|
emailLoaded = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if !emailLoaded {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type Invites map[string]Invite
|
|
|
|
func (st *Storage) loadInvites() error {
|
|
return loadJSON(st.invite_path, &st.invites)
|
|
}
|
|
|
|
func (st *Storage) storeInvites() error {
|
|
return storeJSON(st.invite_path, st.invites)
|
|
}
|
|
|
|
func (st *Storage) loadEmails() error {
|
|
return loadJSON(st.emails_path, &st.emails)
|
|
}
|
|
|
|
func (st *Storage) storeEmails() error {
|
|
return storeJSON(st.emails_path, st.emails)
|
|
}
|
|
|
|
func (st *Storage) loadPolicy() error {
|
|
return loadJSON(st.policy_path, &st.policy)
|
|
}
|
|
|
|
func (st *Storage) storePolicy() error {
|
|
return storeJSON(st.policy_path, st.policy)
|
|
}
|
|
|
|
func (st *Storage) loadConfiguration() error {
|
|
return loadJSON(st.configuration_path, &st.configuration)
|
|
}
|
|
|
|
func (st *Storage) storeConfiguration() error {
|
|
return storeJSON(st.configuration_path, st.configuration)
|
|
}
|
|
|
|
func (st *Storage) loadDisplayprefs() error {
|
|
return loadJSON(st.displayprefs_path, &st.displayprefs)
|
|
}
|
|
|
|
func (st *Storage) storeDisplayprefs() error {
|
|
return storeJSON(st.displayprefs_path, st.displayprefs)
|
|
}
|
|
|
|
func (st *Storage) loadOmbiTemplate() error {
|
|
return loadJSON(st.ombi_path, &st.ombi_template)
|
|
}
|
|
|
|
func (st *Storage) storeOmbiTemplate() error {
|
|
return storeJSON(st.ombi_path, st.ombi_template)
|
|
}
|
|
|
|
func (st *Storage) loadProfiles() error {
|
|
err := loadJSON(st.profiles_path, &st.profiles)
|
|
for name, profile := range st.profiles {
|
|
if profile.Default {
|
|
st.defaultProfile = name
|
|
}
|
|
change := false
|
|
if profile.Policy["IsAdministrator"] != nil {
|
|
profile.Admin = profile.Policy["IsAdministrator"].(bool)
|
|
change = true
|
|
}
|
|
if profile.Policy["EnabledFolders"] != nil {
|
|
length := len(profile.Policy["EnabledFolders"].([]interface{}))
|
|
if length == 0 {
|
|
profile.LibraryAccess = "All"
|
|
} else {
|
|
profile.LibraryAccess = strconv.Itoa(length)
|
|
}
|
|
change = true
|
|
}
|
|
if profile.FromUser == "" {
|
|
profile.FromUser = "Unknown"
|
|
change = true
|
|
}
|
|
if change {
|
|
st.profiles[name] = profile
|
|
}
|
|
}
|
|
if st.defaultProfile == "" {
|
|
for n := range st.profiles {
|
|
st.defaultProfile = n
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (st *Storage) storeProfiles() error {
|
|
return storeJSON(st.profiles_path, st.profiles)
|
|
}
|
|
|
|
func (st *Storage) migrateToProfile() error {
|
|
st.loadPolicy()
|
|
st.loadConfiguration()
|
|
st.loadDisplayprefs()
|
|
st.loadProfiles()
|
|
st.profiles["Default"] = Profile{
|
|
Policy: st.policy,
|
|
Configuration: st.configuration,
|
|
Displayprefs: st.displayprefs,
|
|
}
|
|
return st.storeProfiles()
|
|
}
|
|
|
|
func loadJSON(path string, obj interface{}) error {
|
|
var file []byte
|
|
var err error
|
|
file, err = os.ReadFile(path)
|
|
if err != nil {
|
|
file = []byte("{}")
|
|
}
|
|
err = json.Unmarshal(file, &obj)
|
|
if err != nil {
|
|
log.Printf("ERROR: Failed to read \"%s\": %s", path, err)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func storeJSON(path string, obj interface{}) error {
|
|
data, err := json.Marshal(obj)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = os.WriteFile(path, data, 0644)
|
|
if err != nil {
|
|
log.Printf("ERROR: Failed to write to \"%s\": %s", path, err)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// One build of JF 10.7.0 hyphenated user IDs while another one later didn't. These functions will hyphenate/de-hyphenate email storage.
|
|
|
|
func hyphenate(userID string) string {
|
|
if userID[8] == '-' {
|
|
return userID
|
|
}
|
|
return userID[:8] + "-" + userID[8:12] + "-" + userID[12:16] + "-" + userID[16:20] + "-" + userID[20:]
|
|
}
|
|
|
|
func (app *appContext) deHyphenateEmailStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
|
|
jfUsers, status, err := app.jf.GetUsers(false)
|
|
if status != 200 || err != nil {
|
|
return nil, status, err
|
|
}
|
|
newEmails := map[string]interface{}{}
|
|
for _, user := range jfUsers {
|
|
unHyphenated := user["Id"].(string)
|
|
hyphenated := hyphenate(unHyphenated)
|
|
email, ok := old[hyphenated]
|
|
if ok {
|
|
newEmails[unHyphenated] = email
|
|
}
|
|
}
|
|
return newEmails, status, err
|
|
}
|
|
|
|
func (app *appContext) hyphenateEmailStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
|
|
jfUsers, status, err := app.jf.GetUsers(false)
|
|
if status != 200 || err != nil {
|
|
return nil, status, err
|
|
}
|
|
newEmails := map[string]interface{}{}
|
|
for _, user := range jfUsers {
|
|
unstripped := user["Id"].(string)
|
|
stripped := strings.ReplaceAll(unstripped, "-", "")
|
|
email, ok := old[stripped]
|
|
if ok {
|
|
newEmails[unstripped] = email
|
|
}
|
|
}
|
|
return newEmails, status, err
|
|
}
|