Skip to content

Commit

Permalink
Merge pull request #125 from damongolding/task/release
Browse files Browse the repository at this point in the history
0.11.2
  • Loading branch information
damongolding authored Oct 10, 2024
2 parents 27f004c + 34ff28a commit e7f1890
Show file tree
Hide file tree
Showing 30 changed files with 742 additions and 444 deletions.
5 changes: 1 addition & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ ARG TARGETARCH
WORKDIR /app

COPY . .
COPY config.example.yaml /app/config/

RUN go mod download
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-X main.version=${VERSION}" -o dist/kiosk .
Expand All @@ -21,12 +20,10 @@ ENV TERM=xterm-256color
ENV DEBUG_COLORS=true
ENV COLORTERM=truecolor

RUN apk add --no-cache tzdata
RUN apk update && apk add --no-cache tzdata ca-certificates && update-ca-certificates

WORKDIR /

COPY --from=build /app/dist/kiosk .

EXPOSE 3000

ENTRYPOINT ["/kiosk"]
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ kiosk:

| **yaml** | **ENV** | **Value** | **Default** | **Description** |
|-------------------|-------------------------|--------------|-------------|--------------------------------------------------------------------------------------------|
| port | KIOSK_PORT | int | 3000 | Which port Kiosk should use. NOTE that is port will need to be reflected in your compose file e.g. `KIOSK_PORT:HOST_PORT` |
| password | KIOSK_PASSWORD | string | "" | Please see FAQs for more info. If set, requests MUST contain the password in the GET parameters e.g. `http://192.168.0.123:3000?password=PASSWORD`. |
| cache | KIOSK_CACHE | bool | true | Cache selective Immich api calls to reduce unnecessary calls. |
| prefetch | KIOSK_PREFETCH | bool | true | Pre fetch assets in the background so images load much quicker when refresh timer ends. |
Expand Down Expand Up @@ -596,8 +597,9 @@ Then to access Kiosk you MUST add the password param in your URL e.g. http://{UR
- PWA
- monitor config file changes
- make apiCalls more resilient
- Ken Burns
- Ken Burns
- splitview horizontal mode
- docker/immich healthcheck?
------
Expand Down
13 changes: 9 additions & 4 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,15 @@ disable_screensaver: false # Ask browser to request a lock that prevents device

# Asset sources
show_archived: false # Allow assets marked as archived to be displayed.
person: # ID(s) of person or people to display
- ""
album: # ID(s) of album or albums to display
- ""

# ID(s) of person or people to display
person:
- "PERSON_ID"

# ID(s) of album or albums to display
album:
- "ALBUM_ID"

# UI
disable_ui: false # this is just a shortcut for all ui elements (show_time, show_date, show_image_time, show_image_date)
hide_cursor: false # Hide cursor/mouse via CSS.
Expand Down
157 changes: 144 additions & 13 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,15 @@ package config
import (
"encoding/json"
"errors"
"os"
"strings"
"sync"
"time"

"github.com/charmbracelet/log"
"github.com/mcuadros/go-defaults"
"github.com/spf13/viper"
"gopkg.in/yaml.v3"

"github.com/labstack/echo/v4"
)
Expand All @@ -36,9 +40,13 @@ const (
defaultImmichPort = "2283"
defaultScheme = "http://"
DefaultDateLayout = "02/01/2006"
defaultConfigFile = "config.yaml"
)

type KioskSettings struct {
// Port which port to use
Port int `mapstructure:"port" default:"3000"`

// Cache enable/disable api call and image caching
Cache bool `mapstructure:"cache" default:"true"`

Expand All @@ -57,6 +65,15 @@ type KioskSettings struct {
}

type Config struct {
// v is the viper instance used for configuration management
v *viper.Viper
// mu is a mutex used to ensure thread-safe access to the configuration
mu *sync.Mutex
// ReloadTimeStamp timestamp for when the last client reload was called for
ReloadTimeStamp string
// configLastModTime stores the last modification time of the configuration file
configLastModTime time.Time

// ImmichApiKey Immich key to access assets
ImmichApiKey string `mapstructure:"immich_api_key" default:""`
// ImmichUrl Immuch base url
Expand Down Expand Up @@ -131,6 +148,8 @@ type Config struct {
ShowImageExif bool `mapstructure:"show_image_exif" query:"show_image_exif" form:"show_image_exif" default:"false"`
// ShowImageLocation display image location data
ShowImageLocation bool `mapstructure:"show_image_location" query:"show_image_location" form:"show_image_location" default:"false"`
// ShowImageID display image ID
ShowImageID bool `mapstructure:"show_image_id" query:"show_image_id" form:"show_image_id" default:"false"`

// Kiosk settings that are unable to be changed via URL queries
Kiosk KioskSettings `mapstructure:"kiosk"`
Expand All @@ -141,11 +160,30 @@ type Config struct {

// New returns a new config pointer instance
func New() *Config {
c := &Config{}
c := &Config{
v: viper.NewWithOptions(viper.ExperimentalBindStruct()),
mu: &sync.Mutex{},
ReloadTimeStamp: time.Now().Format(time.RFC3339),
}
defaults.SetDefaults(c)
info, err := os.Stat(defaultConfigFile)
if err == nil {
c.configLastModTime = info.ModTime()
}
return c
}

// hasConfigChanged checks if the configuration file has been modified since the last check.
func (c *Config) hasConfigChanged() bool {
info, err := os.Stat(defaultConfigFile)
if err != nil {
log.Errorf("Checking config file: %v", err)
return false
}

return info.ModTime().After(c.configLastModTime)
}

// bindEnvironmentVariables binds specific environment variables to their corresponding
// configuration keys in the Viper instance. This function allows for easy mapping
// between environment variables and configuration settings.
Expand All @@ -168,6 +206,7 @@ func bindEnvironmentVariables(v *viper.Viper) error {
configKey string
envVar string
}{
{"kiosk.port", "KIOSK_PORT"},
{"kiosk.password", "KIOSK_PASSWORD"},
{"kiosk.cache", "KIOSK_CACHE"},
{"kiosk.prefetch", "KIOSK_PREFETCH"},
Expand All @@ -189,6 +228,24 @@ func bindEnvironmentVariables(v *viper.Viper) error {
return nil
}

// isValidYAML checks if the given file is a valid YAML file.
func isValidYAML(filename string) bool {
content, err := os.ReadFile(filename)
if err != nil {
log.Errorf("Error reading file: %v", err)
return false
}

var data interface{}
err = yaml.Unmarshal(content, &data)
if err != nil {
log.Fatal(err)
return false
}

return true
}

// checkUrlScheme checks given url has correct scheme and adds http:// if non if found
func (c *Config) checkUrlScheme() {

Expand Down Expand Up @@ -220,50 +277,124 @@ func (c *Config) checkDebuging() {
}
}

func (c *Config) checkAlbumAndPerson() {

newAlbum := []string{}
for _, album := range c.Album {
if album != "" && album != "ALBUM_ID" {
newAlbum = append(newAlbum, album)
}
}
c.Album = newAlbum

newPerson := []string{}
for _, person := range c.Person {
if person != "" && person != "PERSON_ID" {
newPerson = append(newPerson, person)
}
}
c.Person = newPerson
}

// Load loads yaml config file into memory, then loads ENV vars. ENV vars overwrites yaml settings.
func (c *Config) Load() error {
return c.load("config.yaml")
return c.load(defaultConfigFile)
}

// Load loads yaml config file into memory with a custom path, then loads ENV vars. ENV vars overwrites yaml settings.
func (c *Config) LoadWithConfigLocation(configPath string) error {
return c.load(configPath)
}

// WatchConfig starts a goroutine that periodically checks for changes in the configuration file
// and reloads the configuration if changes are detected.
//
// This function performs the following actions:
// 1. Retrieves the initial modification time of the config file.
// 2. Starts a goroutine that runs indefinitely.
// 3. Uses a ticker to check for config changes every 5 seconds.
// 4. If changes are detected, it reloads the configuration and updates the ReloadTimeStamp.
func (c *Config) WatchConfig() {

fileInfo, err := os.Stat(defaultConfigFile)
if os.IsNotExist(err) {
return
}

if fileInfo.IsDir() {
log.Errorf("Config file %s is a directory", defaultConfigFile)
return
}

info, err := os.Stat(defaultConfigFile)
if err != nil {
log.Infof("Error getting initial file info: %v", err)
} else {
c.configLastModTime = info.ModTime()
}

go func() {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()

//nolint:gosimple // Using for-select for ticker and potential future cases
for {
select {
case <-ticker.C:
if c.hasConfigChanged() {
log.Info("Config file changed, reloading config")
c.mu.Lock()
err := c.Load()
if err != nil {
log.Errorf("Reloading config: %v", err)
} else {
c.ReloadTimeStamp = time.Now().Format(time.RFC3339)
info, _ := os.Stat(defaultConfigFile)
c.configLastModTime = info.ModTime()
}
c.mu.Unlock()
}
}
}
}()
}

// load loads yaml config file into memory, then loads ENV vars. ENV vars overwrites yaml settings.
func (c *Config) load(configFile string) error {

v := viper.NewWithOptions(viper.ExperimentalBindStruct())

if err := bindEnvironmentVariables(v); err != nil {
if err := bindEnvironmentVariables(c.v); err != nil {
log.Errorf("binding environment variables: %v", err)
}

v.AddConfigPath(".")
c.v.AddConfigPath(".")

v.SetConfigFile(configFile)
c.v.SetConfigFile(configFile)

v.SetEnvPrefix("kiosk")
c.v.SetEnvPrefix("kiosk")

v.AutomaticEnv()
c.v.AutomaticEnv()

err := v.ReadInConfig()
err := c.v.ReadInConfig()
if err != nil {
log.Debug("config.yaml file not being used")
if _, err := os.Stat(configFile); os.IsNotExist(err) {
log.Infof("Not using %s", configFile)
} else if !isValidYAML(configFile) {
log.Fatal(err)
}
}

err = v.Unmarshal(&c)
err = c.v.Unmarshal(&c)
if err != nil {
log.Error("Environment can't be loaded", "err", err)
return err
}

c.checkRequiredFields()
c.checkAlbumAndPerson()
c.checkUrlScheme()
c.checkDebuging()

return nil

}

// ConfigWithOverrides overwrites base config with ones supplied via URL queries
Expand Down
62 changes: 61 additions & 1 deletion config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ func TestMalformedURLs(t *testing.T) {
t.Setenv("KIOSK_IMMICH_URL", test.KIOSK_IMMICH_URL)
t.Setenv("KIOSK_IMMICH_API_KEY", "12345")

var c Config
c := New()

err := c.Load()
assert.NoError(t, err, "Config load should not return an error")
Expand Down Expand Up @@ -166,3 +166,63 @@ func TestImmichUrlImmichMulitpleAlbum(t *testing.T) {
assert.Contains(t, configWithBaseOnly.Album, "BASE_ALBUM_1", "BASE_ALBUM_1 should be present")
assert.Contains(t, configWithBaseOnly.Album, "BASE_ALBUM_2", "BASE_ALBUM_2 should be present")
}

func TestAlbumAndPerson(t *testing.T) {
testCases := []struct {
name string
inputAlbum []string
inputPerson []string
expectedAlbum []string
expectedPerson []string
}{
{
name: "No empty values",
inputAlbum: []string{"album1", "album2"},
inputPerson: []string{"person1", "person2"},
expectedAlbum: []string{"album1", "album2"},
expectedPerson: []string{"person1", "person2"},
},
{
name: "Empty values in album",
inputAlbum: []string{"album1", "", "album2", ""},
inputPerson: []string{"person1", "person2"},
expectedAlbum: []string{"album1", "album2"},
expectedPerson: []string{"person1", "person2"},
},
{
name: "Empty values in person",
inputAlbum: []string{"album1", "album2"},
inputPerson: []string{"", "person1", "", "person2"},
expectedAlbum: []string{"album1", "album2"},
expectedPerson: []string{"person1", "person2"},
},
{
name: "Empty values in both",
inputAlbum: []string{"", "album1", "", "album2"},
inputPerson: []string{"person1", "", "", "person2"},
expectedAlbum: []string{"album1", "album2"},
expectedPerson: []string{"person1", "person2"},
},
{
name: "All empty values",
inputAlbum: []string{"", "", ""},
inputPerson: []string{"", "", ""},
expectedAlbum: []string{},
expectedPerson: []string{},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
c := &Config{
Album: tc.inputAlbum,
Person: tc.inputPerson,
}

c.checkAlbumAndPerson()

assert.Equal(t, tc.expectedAlbum, c.Album, "Album mismatch")
assert.Equal(t, tc.expectedPerson, c.Person, "Person mismatch")
})
}
}
Loading

0 comments on commit e7f1890

Please sign in to comment.