Compare commits
No commits in common. "e8edfdaf8318ae3110e3cff13eb1acaa94b248d3" and "5d9b028c29f46f9c7cfbb77aa9c8154d1cd0c9ca" have entirely different histories.
e8edfdaf83
...
5d9b028c29
19 changed files with 597 additions and 452 deletions
36
.vscode/launch.json
vendored
36
.vscode/launch.json
vendored
|
@ -2,23 +2,37 @@
|
|||
"version": "0.0.1",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "slideshow",
|
||||
"name": "slideshow-scaled",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}/main.go",
|
||||
"env":{
|
||||
"SLIDESHOW_API": "localhost:3000",
|
||||
"SLIDESHOW_INTERVAL": "3",
|
||||
"SLIDESHOW_RESOLUTION": "",
|
||||
"SLIDESHOW_PALETTE": "",
|
||||
"SLIDESHOW_PALETTE_ALGORITHM": "",
|
||||
"SLIDESHOW_PALETTE_COLORS": "",
|
||||
"SLIDESHOW_SCRIPT": "",
|
||||
"SLIDESHOW_SCRIPT_ARGS": "",
|
||||
"SLIDESHOW_SCRIPT_ASYNC": "",
|
||||
"SLIDESHOW_SCRIPT_STAGE": "",
|
||||
"SLIDESHOW_INTERVAL": "10",
|
||||
"SLIDESHOW_DIRECTORY": "/home/velvettear/images",
|
||||
"SLIDESHOW_SCANINTERVAL": "300",
|
||||
"SLIDESHOW_RESOLUTION": "600x1024",
|
||||
"SLIDESHOW_LOGLEVEL": "debug",
|
||||
"SLIDESHOW_PALETTE": "/tmp/.slideshow.palette",
|
||||
"SLIDESHOW_PALETTE_ALGORITHM": "wsm",
|
||||
"SLIDESHOW_PALETTE_COLORS": "32"
|
||||
},
|
||||
"console": "integratedTerminal"
|
||||
},
|
||||
{
|
||||
"name": "slideshow-unscaled",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}/main.go",
|
||||
"env":{
|
||||
"SLIDESHOW_INTERVAL": "10",
|
||||
"SLIDESHOW_DIRECTORY": "/home/velvettear/images",
|
||||
"SLIDESHOW_SCANINTERVAL": "10",
|
||||
"SLIDESHOW_LOGLEVEL": "debug",
|
||||
"SLIDESHOW_PALETTE": "/tmp/.slideshow.palette",
|
||||
"SLIDESHOW_PALETTE_ALGORITHM": "wsm",
|
||||
"SLIDESHOW_PALETTE_COLORS": "16"
|
||||
},
|
||||
"console": "integratedTerminal"
|
||||
}
|
||||
|
|
25
README.md
25
README.md
|
@ -1,11 +1,11 @@
|
|||
# slideshow
|
||||
|
||||
client for the [slideshow-api](https://git.velvettear.de/velvettear/slideshow-api).
|
||||
sets (scaled) images as background via 'feh', retrieves color palettes (and executes a script).
|
||||
a simple cli application to start a background image slideshow using 'feh'.
|
||||
|
||||
## requirements
|
||||
|
||||
- [feh](https://feh.finalrewind.org/)
|
||||
- [ImageMagick](https://imagemagick.org/) (optional)
|
||||
|
||||
## configuration
|
||||
|
||||
|
@ -13,32 +13,19 @@ configuration is entirely done via environment variables.
|
|||
|
||||
| variable | default | description |
|
||||
| --------------------------- | ----------------- | -----------------------------------------------------------|
|
||||
| SLIDESHOW_API | | the address of the slideshow-api server |
|
||||
| SLIDESHOW_INTERVAL | 60 | the interval of the slideshow in seconds |
|
||||
| SLIDESHOW_DIRECTORY | "$HOME" | path to a directory containing images |
|
||||
| SLIDESHOW_SCANINTERVAL | 60 | the interval for directory scans in seconds
|
||||
| SLIDESHOW_RESOLUTION | | the resolution to which images are scaled (i.e. 1920x1080) |
|
||||
| SLIDESHOW_PALETTE | | path to a file where the color palette will be stored |
|
||||
| SLIDESHOW_PALETTE_ALGORITHM | "wsm" | the algorithm used to generate the color palette |
|
||||
| SLIDESHOW_PALETTE_COLORS | 16 | the amount of colors generated |
|
||||
| SLIDESHOW_SCRIPT | | path to a script to execute each loop |
|
||||
| SLIDESHOW_SCRIPT_ARGS | | arguments to pass to the script |
|
||||
| SLIDESHOW_SCRIPT_ASYNC | false | run the script asynchronously (in a goroutine) |
|
||||
| SLIDESHOW_SCRIPT_STAGE | | the stage at which the script is executed |
|
||||
| SLIDESHOW_LOGLEVEL | "info" | the log level |
|
||||
|
||||
**note:**
|
||||
if no resolution is set the images will be displayed as they are (without any scaling).
|
||||
|
||||
- if `SLIDESHOW_RESOLUTION` is unset images will be requested in their original resolution.
|
||||
- if `SLIDESHOW_PALETTE` is unset no color palettes will be requested.
|
||||
- if `SLIDESHOW_SCRIPT_STAGE` is unset the script (if specified) will be executed **after** the background image has been set **and** the color palette file has been generated.
|
||||
|
||||
### stages:
|
||||
|
||||
- `pre_image`: executes the script **before** the background image will be set.
|
||||
- `post_image`: executes the script **after** the background image has been set.
|
||||
- `pre_palette`: executes the script **before** the color palette file will be generated.
|
||||
- `post_palette`: executes the script **after** the color palette has been generated.
|
||||
|
||||
### log levels:
|
||||
**available log levels:**
|
||||
|
||||
- `debug`
|
||||
- `info`
|
||||
|
|
3
go.mod
3
go.mod
|
@ -6,7 +6,8 @@ require git.velvettear.de/velvettear/loggo v0.0.0-20231113084149-980a00b4e084
|
|||
|
||||
require (
|
||||
github.com/fatih/color v1.16.0 // indirect
|
||||
github.com/kennykarnama/color-thief v0.0.0-20230222041546-c1bf65ec0808
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
golang.org/x/sys v0.15.0 // indirect
|
||||
golang.org/x/sys v0.14.0 // indirect
|
||||
)
|
||||
|
|
4
go.sum
4
go.sum
|
@ -2,6 +2,8 @@ git.velvettear.de/velvettear/loggo v0.0.0-20231113084149-980a00b4e084 h1:13S20q+
|
|||
git.velvettear.de/velvettear/loggo v0.0.0-20231113084149-980a00b4e084/go.mod h1:Jjjno0vz7v1Y6tCnpQHnq2TVL2+5m7TXkmNNYYREIMo=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/kennykarnama/color-thief v0.0.0-20230222041546-c1bf65ec0808 h1:9JJaKNm4eDnB/ad7rWfejfMw+1ufpchM8Eik9VcQzUQ=
|
||||
github.com/kennykarnama/color-thief v0.0.0-20230222041546-c1bf65ec0808/go.mod h1:9qLIEhoYLAXPjhQQpgR0nVnhJxNXBtAqRZAp2Dj87WQ=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
|
@ -11,5 +13,3 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
|
||||
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
|
|
105
images/cache.go
Normal file
105
images/cache.go
Normal file
|
@ -0,0 +1,105 @@
|
|||
package images
|
||||
|
||||
// import (
|
||||
// "runtime"
|
||||
// "strconv"
|
||||
// "sync"
|
||||
// "time"
|
||||
|
||||
// "git.velvettear.de/velvettear/image-frame/config"
|
||||
// "git.velvettear.de/velvettear/loggo"
|
||||
// )
|
||||
|
||||
// // cache for scaled images
|
||||
// var cache []scaledImage
|
||||
|
||||
// // timestamp of the next cache rotation
|
||||
// var NextRotation time.Time
|
||||
|
||||
// // get the previous image from the history and set it as the first scaled image in the cache
|
||||
// func SetPreviousImage() error {
|
||||
// previousImage, error := getLatestFromHistory()
|
||||
// if error != nil {
|
||||
// return error
|
||||
// }
|
||||
// var tmpCache []scaledImage
|
||||
// tmpCache = append(tmpCache, previousImage)
|
||||
// tmpCache = append(tmpCache, cache...)
|
||||
// cache = tmpCache
|
||||
// tmpCache = nil
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// // get the first scaled image from the cache
|
||||
// func GetCachedImage() scaledImage {
|
||||
// return cache[0]
|
||||
// }
|
||||
|
||||
// // replace the first element in the cache with the last one and add a new scaled image to the cache
|
||||
// func RotateCache() {
|
||||
// addToHistory(GetCachedImage())
|
||||
// loggo.Debug("removing first element from image cache...")
|
||||
// cacheSize := len(cache)
|
||||
// if cacheSize == 1 {
|
||||
// cache = nil
|
||||
// cacheImages()
|
||||
// return
|
||||
// }
|
||||
// cacheSize--
|
||||
// cache[0] = cache[cacheSize]
|
||||
// cache = cache[:cacheSize]
|
||||
// go cacheImages()
|
||||
// }
|
||||
|
||||
// // start the "slideshow" in a goroutine - rotate the cache based on the set interval
|
||||
// func startSlideshow() {
|
||||
// go func() {
|
||||
// interval := time.Duration(config.GetImageSlideshowInterval()) * time.Second
|
||||
// for {
|
||||
// NextRotation = time.Now().Add(interval)
|
||||
// time.Sleep(interval)
|
||||
// RotateCache()
|
||||
// }
|
||||
// }()
|
||||
// }
|
||||
|
||||
// // fill the cache with scaled images
|
||||
// func cacheImages() {
|
||||
// timestamp := time.Now().UnixMilli()
|
||||
// imageCount := len(images)
|
||||
// cacheSize := len(cache)
|
||||
// cacheLimit := config.GetImageCache()
|
||||
// if imageCount < cacheLimit {
|
||||
// cacheLimit = imageCount
|
||||
// }
|
||||
// imagesToCache := cacheLimit - cacheSize
|
||||
// if imagesToCache <= 0 {
|
||||
// return
|
||||
// }
|
||||
// concurrency := runtime.NumCPU()
|
||||
// if imagesToCache < concurrency {
|
||||
// concurrency = imagesToCache
|
||||
// }
|
||||
// loggo.Debug("filling image cache with "+strconv.Itoa(imagesToCache)+" element(s)", "concurrency: "+strconv.Itoa(concurrency))
|
||||
// var waitgroup sync.WaitGroup
|
||||
// waitgroup.Add(imagesToCache)
|
||||
// channel := make(chan struct{}, concurrency)
|
||||
// var cached int
|
||||
// for cached = 0; cached < imagesToCache; cached++ {
|
||||
// channel <- struct{}{}
|
||||
// var randomScaledImage scaledImage
|
||||
// randomScaledImage.Name = getRandomImage()
|
||||
// if randomScaledImage.isCached() {
|
||||
// continue
|
||||
// }
|
||||
// go func(randomScaledImage scaledImage) {
|
||||
// randomScaledImage.Data = scale(randomScaledImage.Name, config.GetImageWidth(), config.GetImageHeight())
|
||||
// cache = append(cache, randomScaledImage)
|
||||
// loggo.Debug("added scaled image '" + randomScaledImage.Name + "' to cache")
|
||||
// <-channel
|
||||
// waitgroup.Done()
|
||||
// }(randomScaledImage)
|
||||
// }
|
||||
// waitgroup.Wait()
|
||||
// loggo.DebugTimed("filled image cache with "+strconv.Itoa(cached)+" images", timestamp)
|
||||
// }
|
40
images/history.go
Normal file
40
images/history.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
package images
|
||||
|
||||
// import (
|
||||
// "errors"
|
||||
// "strconv"
|
||||
// "time"
|
||||
|
||||
// "git.velvettear.de/velvettear/image-frame/config"
|
||||
// "git.velvettear.de/velvettear/loggo"
|
||||
// )
|
||||
|
||||
// // internal history of displayed images
|
||||
// var history []scaledImage
|
||||
|
||||
// // add a image to the history
|
||||
// func addToHistory(scaledImage scaledImage) {
|
||||
// timestamp := time.Now().UnixMilli()
|
||||
// historyLimit := config.GetImageSlideshowHistory()
|
||||
// if historyLimit <= 0 {
|
||||
// return
|
||||
// }
|
||||
// history = append(history, scaledImage)
|
||||
// diff := len(history) - historyLimit
|
||||
// if diff > 0 {
|
||||
// history = history[diff:]
|
||||
// }
|
||||
// loggo.DebugTimed("added image to history", timestamp, "history size: "+strconv.Itoa(len(history)))
|
||||
// }
|
||||
|
||||
// // get and remove the latest image from history
|
||||
// func getLatestFromHistory() (scaledImage, error) {
|
||||
// var scaledImage scaledImage
|
||||
// if len(history) == 0 {
|
||||
// return scaledImage, errors.New("history is empty")
|
||||
// }
|
||||
// index := len(history) - 1
|
||||
// scaledImage = history[index]
|
||||
// history = history[:index]
|
||||
// return scaledImage, nil
|
||||
// }
|
7
images/initialize.go
Normal file
7
images/initialize.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
package images
|
||||
|
||||
// func Initialize() {
|
||||
// scanForImages()
|
||||
// cacheImages()
|
||||
// startSlideshow()
|
||||
// }
|
31
images/scaledimage.go
Normal file
31
images/scaledimage.go
Normal file
|
@ -0,0 +1,31 @@
|
|||
package images
|
||||
|
||||
// import (
|
||||
// "math/rand"
|
||||
// )
|
||||
|
||||
// // struct for scaled images
|
||||
// type scaledImage struct {
|
||||
// Name string
|
||||
// Data []byte
|
||||
// }
|
||||
|
||||
// // get a random image
|
||||
// func getRandomImage() string {
|
||||
// return images[rand.Intn(len(images)-1)]
|
||||
// }
|
||||
|
||||
// // check if the scaled image is already cached
|
||||
// func (scaledImage *scaledImage) isCached() bool {
|
||||
// return isCached(scaledImage.Name)
|
||||
// }
|
||||
|
||||
// // check (by name) if the scaled image is already cached
|
||||
// func isCached(scaledImageName string) bool {
|
||||
// for _, cachedImage := range cache {
|
||||
// if cachedImage.Name == scaledImageName {
|
||||
// return true
|
||||
// }
|
||||
// }
|
||||
// return false
|
||||
// }
|
|
@ -2,7 +2,6 @@ package config
|
|||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
@ -10,39 +9,49 @@ import (
|
|||
"git.velvettear.de/velvettear/loggo"
|
||||
)
|
||||
|
||||
var ApiAddress string
|
||||
var Interval time.Duration
|
||||
var Directory string
|
||||
var ScanInterval time.Duration
|
||||
var Resolution string
|
||||
var PaletteFile string
|
||||
var PaletteAlgorithm string
|
||||
var PaletteAlgorithm int
|
||||
var PaletteColors int
|
||||
var Script string
|
||||
var ScriptArgs []string
|
||||
var ScriptAsync bool
|
||||
var ScriptStage string
|
||||
|
||||
// initialize the config
|
||||
func Initialize() {
|
||||
loggo.SetLogLevelByName(os.Getenv("SLIDESHOW_LOGLEVEL"))
|
||||
ApiAddress = os.Getenv("SLIDESHOW_API")
|
||||
if len(ApiAddress) == 0 {
|
||||
loggo.Fatal("no adress of the slideshow-api server has been specified")
|
||||
}
|
||||
if !strings.HasPrefix(ApiAddress, "http://") && !strings.HasPrefix(ApiAddress, "https://") {
|
||||
ApiAddress = "http://" + ApiAddress
|
||||
}
|
||||
tmpInt, _ := strconv.Atoi(os.Getenv("SLIDESHOW_INTERVAL"))
|
||||
if tmpInt <= 0 {
|
||||
tmpInt = 60
|
||||
}
|
||||
Interval = time.Duration(tmpInt) * time.Second
|
||||
Directory = os.Getenv("SLIDESHOW_DIRECTORY")
|
||||
if len(Directory) == 0 {
|
||||
tmp, error := os.UserHomeDir()
|
||||
if error != nil {
|
||||
loggo.Fatal("encountered an error getting the current user's home directory", error.Error())
|
||||
}
|
||||
Directory = tmp
|
||||
}
|
||||
stats, error := os.Stat(Directory)
|
||||
if error != nil {
|
||||
loggo.Fatal("encountered an error checking the directory '"+Directory+"'", error.Error())
|
||||
}
|
||||
if !stats.IsDir() {
|
||||
loggo.Fatal("configured directory '" + Directory + "' is not a valid directory")
|
||||
}
|
||||
tmpInt, _ = strconv.Atoi(os.Getenv("SLIDESHOW_SCANINTERVAL"))
|
||||
if tmpInt <= 0 {
|
||||
tmpInt = 60
|
||||
}
|
||||
ScanInterval = time.Duration(tmpInt) * time.Second
|
||||
Resolution = os.Getenv("SLIDESHOW_RESOLUTION")
|
||||
if len(Resolution) > 0 {
|
||||
width, height, found := strings.Cut(Resolution, "x")
|
||||
if !found {
|
||||
loggo.Fatal("encountered an error parsing the configured resolution, make sure to specify the format like '1920x1080'")
|
||||
}
|
||||
_, error := strconv.Atoi(width)
|
||||
_, error = strconv.Atoi(width)
|
||||
if error != nil {
|
||||
loggo.Fatal("encountered an error parsing the configured width '" + width + "'")
|
||||
}
|
||||
|
@ -52,67 +61,17 @@ func Initialize() {
|
|||
}
|
||||
}
|
||||
PaletteFile = os.Getenv("SLIDESHOW_PALETTE")
|
||||
PaletteAlgorithm = os.Getenv("SLIDESHOW_PALETTE_ALGORITHM")
|
||||
tmpString := os.Getenv("SLIDESHOW_PALETTE_ALGORITHM")
|
||||
if strings.ToLower(tmpString) == "wu" {
|
||||
PaletteAlgorithm = 0
|
||||
} else {
|
||||
PaletteAlgorithm = 1
|
||||
}
|
||||
tmpInt, _ = strconv.Atoi(os.Getenv("SLIDESHOW_PALETTE_COLORS"))
|
||||
if tmpInt <= 0 {
|
||||
tmpInt = 16
|
||||
}
|
||||
PaletteColors = tmpInt
|
||||
Script = os.Getenv("SLIDESHOW_SCRIPT")
|
||||
ScriptArgs = strings.Split(os.Getenv("SLIDESHOW_SCRIPT_ARGS"), " ")
|
||||
ScriptAsync, _ = strconv.ParseBool(os.Getenv("SLIDESHOW_SCRIPT_ASYNC"))
|
||||
ScriptStage = strings.ToLower(os.Getenv("SLIDESHOW_SCRIPT_STAGE"))
|
||||
checkScript()
|
||||
checkFehCommand()
|
||||
}
|
||||
|
||||
// check if a script has been specified and exists
|
||||
func checkScript() {
|
||||
if len(Script) == 0 {
|
||||
return
|
||||
}
|
||||
stats, error := os.Stat(Script)
|
||||
if error != nil {
|
||||
loggo.Warning("encountered an error getting stats for the script '"+Script+"', script execution is unavailable", error.Error())
|
||||
Script = ""
|
||||
return
|
||||
}
|
||||
if stats.IsDir() {
|
||||
loggo.Warning("the script '"+Script+"' seems to be a directory, script execution is unavailable", error.Error())
|
||||
Script = ""
|
||||
return
|
||||
}
|
||||
if string(stats.Mode().Perm().String()[3]) != "x" {
|
||||
loggo.Warning("the script '"+Script+"' seems to be a directory, script execution is unavailable", error.Error())
|
||||
Script = ""
|
||||
}
|
||||
checkScriptStage()
|
||||
}
|
||||
|
||||
// check if the specified script stage is valid
|
||||
func checkScriptStage() {
|
||||
switch ScriptStage {
|
||||
case "pre_image":
|
||||
fallthrough
|
||||
case "post_image":
|
||||
fallthrough
|
||||
case "pre_palette":
|
||||
fallthrough
|
||||
case "post_palette":
|
||||
return
|
||||
default:
|
||||
ScriptStage = ""
|
||||
}
|
||||
}
|
||||
|
||||
// check if 'feh' is available
|
||||
func checkFehCommand() {
|
||||
cmd := exec.Command("which", "feh")
|
||||
error := cmd.Run()
|
||||
if error == nil {
|
||||
return
|
||||
}
|
||||
loggo.Fatal("could not find command 'feh'")
|
||||
}
|
||||
|
||||
// check if a resolution has been specified
|
||||
|
@ -124,33 +83,3 @@ func IsResolutionSet() bool {
|
|||
func IsPaletteSet() bool {
|
||||
return len(PaletteFile) > 0
|
||||
}
|
||||
|
||||
// check if a script has been specified
|
||||
func IsScriptSet() bool {
|
||||
return len(Script) > 0
|
||||
}
|
||||
|
||||
// check if a script stage has been specified
|
||||
func IsScriptStageSet() bool {
|
||||
return len(ScriptStage) > 0
|
||||
}
|
||||
|
||||
// check if script stage 'pre_image' has been specified
|
||||
func IsScriptStagePreImage() bool {
|
||||
return ScriptStage == "pre_image"
|
||||
}
|
||||
|
||||
// check if script stage 'post_image' has been specified
|
||||
func IsScriptStagePostImage() bool {
|
||||
return ScriptStage == "post_image"
|
||||
}
|
||||
|
||||
// check if script stage 'pre_palette' has been specified
|
||||
func IsScriptStagePrePalette() bool {
|
||||
return ScriptStage == "pre_palette"
|
||||
}
|
||||
|
||||
// check if script stage 'post_palette' has been specified
|
||||
func IsScriptStagePostPalette() bool {
|
||||
return ScriptStage == "post_palette"
|
||||
}
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
package internal
|
||||
|
||||
import "strconv"
|
||||
|
||||
// format bytes
|
||||
func FormatBytes(bytes int64) string {
|
||||
value := float64(bytes)
|
||||
unit := "bytes"
|
||||
if bytes > 1000000 {
|
||||
value = value / 1000000
|
||||
unit = "mega" + unit
|
||||
} else if bytes > 1000 {
|
||||
value = value / 1000
|
||||
unit = "kilo" + unit
|
||||
}
|
||||
return strconv.FormatFloat(value, 'f', 2, 64) + " " + unit
|
||||
}
|
|
@ -1,101 +0,0 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.velvettear.de/velvettear/loggo"
|
||||
"git.velvettear.de/velvettear/slideshow/internal/config"
|
||||
)
|
||||
|
||||
// get the endpoint url of the api
|
||||
func getEndpointUrl(endpoint string) string {
|
||||
url := config.ApiAddress
|
||||
if !strings.HasSuffix(url, "/") && endpoint != "/" {
|
||||
url += "/"
|
||||
}
|
||||
if len(endpoint) > 0 {
|
||||
url += endpoint
|
||||
}
|
||||
if !strings.HasSuffix(url, "/") {
|
||||
url += "/"
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
// request the name of an image from the api
|
||||
func requestImageName() (string, error) {
|
||||
timestamp := time.Now().UnixMilli()
|
||||
var name string
|
||||
url := getEndpointUrl("/")
|
||||
request, error := http.NewRequest("GET", url, nil)
|
||||
if error != nil {
|
||||
return name, error
|
||||
}
|
||||
client := http.Client{}
|
||||
response, error := client.Do(request)
|
||||
if error != nil {
|
||||
return name, error
|
||||
}
|
||||
bytes, error := io.ReadAll(response.Body)
|
||||
if error != nil {
|
||||
return name, error
|
||||
}
|
||||
tmp := make(map[string]interface{})
|
||||
error = json.Unmarshal(bytes, &tmp)
|
||||
if error != nil {
|
||||
return name, error
|
||||
}
|
||||
name = tmp["Content"].(string)
|
||||
loggo.DebugTimed("successfully requested image '"+name+"'", timestamp, "url: "+url)
|
||||
return name, nil
|
||||
}
|
||||
|
||||
// request the data stream of an image by name from the api
|
||||
func requestImageData(name string) (io.ReadCloser, error) {
|
||||
timestamp := time.Now().UnixMilli()
|
||||
var stream io.ReadCloser
|
||||
url := getEndpointUrl("/image/") + name
|
||||
request, error := http.NewRequest("GET", url, nil)
|
||||
if error != nil {
|
||||
return stream, error
|
||||
}
|
||||
queryParameters := request.URL.Query()
|
||||
if config.IsResolutionSet() {
|
||||
queryParameters.Set("resolution", config.Resolution)
|
||||
}
|
||||
request.URL.RawQuery = queryParameters.Encode()
|
||||
client := http.Client{}
|
||||
response, error := client.Do(request)
|
||||
if error != nil {
|
||||
return stream, error
|
||||
}
|
||||
loggo.DebugTimed("successfully requested data stream for image '"+name+"'", timestamp)
|
||||
return response.Body, nil
|
||||
}
|
||||
|
||||
// request the color palette of an image by name from the api as stream
|
||||
func requestColorPalette(name string) (io.ReadCloser, error) {
|
||||
timestamp := time.Now().UnixMilli()
|
||||
var stream io.ReadCloser
|
||||
url := getEndpointUrl("/palette/") + name
|
||||
request, error := http.NewRequest("GET", url, nil)
|
||||
if error != nil {
|
||||
return stream, error
|
||||
}
|
||||
queryParameters := request.URL.Query()
|
||||
queryParameters.Set("colors", strconv.Itoa(config.PaletteColors))
|
||||
queryParameters.Set("algorithm", config.PaletteAlgorithm)
|
||||
request.URL.RawQuery = queryParameters.Encode()
|
||||
client := http.Client{}
|
||||
response, error := client.Do(request)
|
||||
if error != nil {
|
||||
return stream, error
|
||||
}
|
||||
loggo.DebugTimed("successfully requested color palette stream for image '"+name+"'", timestamp)
|
||||
return response.Body, nil
|
||||
}
|
|
@ -1,115 +0,0 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.velvettear.de/velvettear/loggo"
|
||||
"git.velvettear.de/velvettear/slideshow/internal/config"
|
||||
)
|
||||
|
||||
var loop int64
|
||||
|
||||
// start the loop for the slideshow
|
||||
func StartSlideshow() {
|
||||
for {
|
||||
timestamp := time.Now().UnixMilli()
|
||||
loop++
|
||||
imageLoop()
|
||||
loggo.InfoTimed("slideshow loop #"+strconv.FormatInt(loop, 10)+" finished", timestamp)
|
||||
loggo.Debug("sleeping for " + strconv.FormatFloat(config.Interval.Seconds(), 'f', 0, 64) + " seconds...")
|
||||
time.Sleep(config.Interval)
|
||||
}
|
||||
}
|
||||
|
||||
// the image loop of the slideshoow
|
||||
func imageLoop() {
|
||||
name, error := requestImageName()
|
||||
if error != nil {
|
||||
loggo.Error("encountered an error requesting the name of an image", error.Error())
|
||||
return
|
||||
}
|
||||
var waitgroup sync.WaitGroup
|
||||
waitgroup.Add(2)
|
||||
go setBackground(name, &waitgroup)
|
||||
go exportColorPalette(name, &waitgroup)
|
||||
waitgroup.Wait()
|
||||
if !config.IsScriptStageSet() {
|
||||
runScript(false)
|
||||
}
|
||||
}
|
||||
|
||||
// encapsulated method to set the background image in a goroutine
|
||||
func setBackground(name string, waitgroup *sync.WaitGroup) {
|
||||
timestamp := time.Now().UnixMilli()
|
||||
defer waitgroup.Done()
|
||||
if config.IsScriptStagePreImage() {
|
||||
runScript(false)
|
||||
}
|
||||
stream, error := requestImageData(name)
|
||||
if error != nil {
|
||||
loggo.Error("encountered an error requesting the data stream for image '"+name+"'", error.Error())
|
||||
return
|
||||
}
|
||||
error = streamToFeh(stream)
|
||||
if error != nil {
|
||||
loggo.Error("encountered an error piping the data stream for image '"+name+"' to 'feh'", error.Error())
|
||||
}
|
||||
loggo.InfoTimed("set new background image '"+name+"' via 'feh'", timestamp)
|
||||
if config.IsScriptStagePostImage() {
|
||||
runScript(false)
|
||||
}
|
||||
}
|
||||
|
||||
// encapsulated method to set the background image in a goroutine
|
||||
func exportColorPalette(name string, waitgroup *sync.WaitGroup) {
|
||||
timestamp := time.Now().UnixMilli()
|
||||
defer waitgroup.Done()
|
||||
if !config.IsPaletteSet() {
|
||||
return
|
||||
}
|
||||
if config.IsScriptStagePrePalette() {
|
||||
runScript(false)
|
||||
}
|
||||
stream, error := requestColorPalette(name)
|
||||
if error != nil {
|
||||
loggo.Error("encountered an error requesting the data stream for the color palette of image '"+name+"'", error.Error())
|
||||
return
|
||||
}
|
||||
error = streamToFile(stream, config.PaletteFile)
|
||||
if error != nil {
|
||||
loggo.Error("encountered an error piping the data stream for the color palette of image '"+name+"' to file '"+config.PaletteFile+"'", error.Error())
|
||||
}
|
||||
loggo.InfoTimed("exported color palette of image '"+name+"' to '"+config.PaletteFile+"'", timestamp)
|
||||
if config.IsScriptStagePostPalette() {
|
||||
runScript(false)
|
||||
}
|
||||
}
|
||||
|
||||
// encapsulated method to run the defined script
|
||||
func runScript(running bool) {
|
||||
if !running && config.ScriptAsync {
|
||||
go runScript(true)
|
||||
return
|
||||
}
|
||||
timestamp := time.Now().UnixMilli()
|
||||
if !config.IsScriptSet() {
|
||||
return
|
||||
}
|
||||
cmd := exec.Command(config.Script, config.ScriptArgs...)
|
||||
error := cmd.Run()
|
||||
if error != nil {
|
||||
loggo.Error("encountered an error executing the script '"+config.Script+"'", error.Error())
|
||||
return
|
||||
}
|
||||
var extras []string
|
||||
if config.ScriptAsync {
|
||||
extras = append(extras, "async: "+strconv.FormatBool(config.ScriptAsync))
|
||||
}
|
||||
if config.IsScriptStageSet() {
|
||||
extras = append(extras, "stage: "+config.ScriptStage)
|
||||
}
|
||||
loggo.InfoTimed("executed script '"+config.Script+"'", timestamp, extras...)
|
||||
}
|
86
internal/slideshow/palette.go
Normal file
86
internal/slideshow/palette.go
Normal file
|
@ -0,0 +1,86 @@
|
|||
package slideshow
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.velvettear.de/velvettear/loggo"
|
||||
"git.velvettear.de/velvettear/slideshow/internal/config"
|
||||
color_thief "github.com/kennykarnama/color-thief"
|
||||
)
|
||||
|
||||
// write the base16 color palette to a file
|
||||
func exportPalette(colors []color.Color) {
|
||||
if len(colors) == 0 || !config.IsPaletteSet() {
|
||||
return
|
||||
}
|
||||
timestamp := time.Now().UnixMilli()
|
||||
var palette string
|
||||
for index, color := range colors {
|
||||
if index > 0 {
|
||||
palette += "\n"
|
||||
}
|
||||
tmp := strconv.Itoa(index)
|
||||
if len(tmp) < 2 {
|
||||
tmp = "0" + tmp
|
||||
}
|
||||
tmp = "SLIDESHOW_COLOR" + tmp
|
||||
palette += tmp + "=\"" + rgbToHex(color) + "\""
|
||||
}
|
||||
error := os.WriteFile(config.PaletteFile, []byte(palette), 0775)
|
||||
if error != nil {
|
||||
loggo.ErrorTimed("encountered an error exporting a color palette", timestamp, error.Error())
|
||||
} else {
|
||||
loggo.DebugTimed("exported color palette to filesystem", timestamp, "path: "+config.PaletteFile)
|
||||
}
|
||||
}
|
||||
|
||||
// extract the given amount of dominant colors of raw image bytes
|
||||
func getColorPaletteRaw(data []byte) ([]color.Color, error) {
|
||||
var colors []color.Color
|
||||
if !config.IsPaletteSet() {
|
||||
return colors, nil
|
||||
}
|
||||
timestamp := time.Now().UnixMilli()
|
||||
amount := config.PaletteColors
|
||||
img, _, error := image.Decode(bytes.NewReader(data))
|
||||
if error != nil {
|
||||
loggo.ErrorTimed("encountered an error decoding the provided raw image bytes to an image", timestamp, error.Error())
|
||||
return colors, error
|
||||
}
|
||||
colors, error = color_thief.GetPalette(img, amount, config.PaletteAlgorithm)
|
||||
if error != nil {
|
||||
loggo.ErrorTimed("encountered an error generating the color palette from raw image bytes", timestamp, "colors: "+strconv.Itoa(amount))
|
||||
} else {
|
||||
loggo.DebugTimed("generated color palette from raw image bytes", timestamp, "colors: "+strconv.Itoa(amount))
|
||||
}
|
||||
return colors, error
|
||||
}
|
||||
|
||||
// extract the given amount of dominant colors of an image
|
||||
func getColorPalette(image string) ([]color.Color, error) {
|
||||
var colors []color.Color
|
||||
if !config.IsPaletteSet() {
|
||||
return colors, nil
|
||||
}
|
||||
timestamp := time.Now().UnixMilli()
|
||||
amount := config.PaletteColors
|
||||
colors, error := color_thief.GetPaletteFromFile(image, amount, config.PaletteAlgorithm)
|
||||
if error != nil {
|
||||
loggo.ErrorTimed("encountered an error generating the color palette from image", timestamp, "image: "+image, "colors: "+strconv.Itoa(amount))
|
||||
} else {
|
||||
loggo.DebugTimed("generated color palette from image", timestamp, "image: "+image, "colors: "+strconv.Itoa(amount))
|
||||
}
|
||||
return colors, error
|
||||
}
|
||||
|
||||
// parse rgb color values to hex
|
||||
func rgbToHex(color color.Color) string {
|
||||
r, g, b, _ := color.RGBA()
|
||||
return fmt.Sprintf("#%02x%02x%02x", uint8(r>>8), uint8(g>>8), uint8(b>>8))
|
||||
}
|
44
internal/slideshow/scale.go
Normal file
44
internal/slideshow/scale.go
Normal file
|
@ -0,0 +1,44 @@
|
|||
package slideshow
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.velvettear.de/velvettear/loggo"
|
||||
"git.velvettear.de/velvettear/slideshow/internal/config"
|
||||
)
|
||||
|
||||
// scale an image
|
||||
func scale(image string) ([]byte, error) {
|
||||
timestamp := time.Now().UnixMilli()
|
||||
cmd := exec.Command("convert", image, "-resize", config.Resolution+"^", "-gravity", "center", "-extent", config.Resolution, "-")
|
||||
stdout, stdoutError := cmd.StdoutPipe()
|
||||
stderr, stderrError := cmd.StderrPipe()
|
||||
var data []byte
|
||||
cmd.Start()
|
||||
if stdoutError != nil {
|
||||
return data, stdoutError
|
||||
}
|
||||
if stderrError != nil {
|
||||
return data, stdoutError
|
||||
}
|
||||
data, stdoutError = io.ReadAll(stdout)
|
||||
if stdoutError != nil {
|
||||
return data, stdoutError
|
||||
}
|
||||
errorBytes, stderrError := io.ReadAll(stderr)
|
||||
if stderrError != nil {
|
||||
return data, stdoutError
|
||||
}
|
||||
cmd.Wait()
|
||||
errorMessage := strings.TrimSpace(string(errorBytes))
|
||||
if len(errorMessage) > 0 {
|
||||
return data, errors.New(errorMessage)
|
||||
}
|
||||
loggo.DebugTimed("successfully scaled image", timestamp, "image: "+image, "resolution: "+config.Resolution, "size: "+strconv.Itoa(len(data)))
|
||||
return data, nil
|
||||
}
|
138
internal/slideshow/slideshow.go
Normal file
138
internal/slideshow/slideshow.go
Normal file
|
@ -0,0 +1,138 @@
|
|||
package slideshow
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"image/color"
|
||||
"io"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.velvettear.de/velvettear/loggo"
|
||||
"git.velvettear.de/velvettear/slideshow/internal/config"
|
||||
"git.velvettear.de/velvettear/slideshow/scanner"
|
||||
)
|
||||
|
||||
// remember last shown image
|
||||
var previousImage string
|
||||
|
||||
// start the slideshow
|
||||
func Start() {
|
||||
loggo.Info("starting the image slideshow...", "interval: "+strconv.FormatFloat(config.Interval.Seconds(), 'f', 0, 64)+" seconds")
|
||||
var sleepTime time.Duration
|
||||
var scaleTime time.Duration
|
||||
scaleImages := config.IsResolutionSet()
|
||||
for {
|
||||
var image string
|
||||
var palette []color.Color
|
||||
var data []byte
|
||||
for {
|
||||
image = scanner.GetRandomImage()
|
||||
if image != previousImage {
|
||||
break
|
||||
}
|
||||
}
|
||||
if scaleImages {
|
||||
scaleTimestamp := time.Now()
|
||||
tmp, error := scale(image)
|
||||
if error != nil {
|
||||
loggo.Error("encountered an error scaling an image", "image: "+image, error.Error())
|
||||
continue
|
||||
}
|
||||
data = tmp
|
||||
scaleTime = time.Since(scaleTimestamp)
|
||||
palette, _ = getColorPaletteRaw(data)
|
||||
} else {
|
||||
palette, _ = getColorPalette(image)
|
||||
}
|
||||
if sleepTime > 0 {
|
||||
loggo.Debug("sleeping for " + strconv.FormatInt(sleepTime.Milliseconds(), 10) + "ms before next image will be displayed...")
|
||||
time.Sleep(sleepTime)
|
||||
}
|
||||
go exportPalette(palette)
|
||||
if scaleImages {
|
||||
error := setBackgroundPiped(data)
|
||||
if error != nil {
|
||||
loggo.Error("encountered an error setting the background via pipe to feh's stdin", error.Error())
|
||||
}
|
||||
loggo.Info("set new scaled background image", "image: "+image, "resolution: "+config.Resolution)
|
||||
} else {
|
||||
error := setBackgroundImage(image)
|
||||
if error != nil {
|
||||
loggo.Error("encountered an error setting the background image", "image: "+image, error.Error())
|
||||
}
|
||||
loggo.Info("set new background image", "image: "+image)
|
||||
}
|
||||
sleepTime = config.Interval - scaleTime
|
||||
}
|
||||
}
|
||||
|
||||
// set the background image via 'feh'
|
||||
func setBackgroundImage(image string) error {
|
||||
cmd := exec.Command("feh", "--no-fehbg", "--bg-fill", image)
|
||||
stdout, stdoutError := cmd.StdoutPipe()
|
||||
stderr, stderrError := cmd.StderrPipe()
|
||||
cmd.Start()
|
||||
if stdoutError != nil {
|
||||
return stdoutError
|
||||
}
|
||||
if stderrError != nil {
|
||||
return stderrError
|
||||
}
|
||||
_, stdoutError = io.ReadAll(stdout)
|
||||
if stdoutError != nil {
|
||||
return stdoutError
|
||||
}
|
||||
errorBytes, stderrError := io.ReadAll(stderr)
|
||||
if stderrError != nil {
|
||||
return stderrError
|
||||
}
|
||||
cmd.Wait()
|
||||
error := strings.TrimSpace(string(errorBytes))
|
||||
if len(error) > 0 {
|
||||
return errors.New(error)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// pipe data to 'feh' via stdin to set the background image
|
||||
func setBackgroundPiped(data []byte) error {
|
||||
cmd := exec.Command("feh", "--no-fehbg", "--bg-fill", "-")
|
||||
stdin, stdinError := cmd.StdinPipe()
|
||||
stdout, stdoutError := cmd.StdoutPipe()
|
||||
stderr, stderrError := cmd.StderrPipe()
|
||||
cmd.Start()
|
||||
if stdinError != nil {
|
||||
return stdinError
|
||||
}
|
||||
if stdoutError != nil {
|
||||
return stdoutError
|
||||
}
|
||||
if stderrError != nil {
|
||||
return stderrError
|
||||
}
|
||||
defer stdin.Close()
|
||||
defer stdout.Close()
|
||||
defer stderr.Close()
|
||||
_, stdinError = stdin.Write(data)
|
||||
if stdinError != nil {
|
||||
return stdinError
|
||||
} else {
|
||||
stdin.Close()
|
||||
}
|
||||
_, stdoutError = io.ReadAll(stdout)
|
||||
if stdoutError != nil {
|
||||
return stdoutError
|
||||
}
|
||||
errorBytes, stderrError := io.ReadAll(stderr)
|
||||
if stderrError != nil {
|
||||
return stderrError
|
||||
}
|
||||
cmd.Wait()
|
||||
error := strings.TrimSpace(string(errorBytes))
|
||||
if len(error) > 0 {
|
||||
return errors.New(error)
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.velvettear.de/velvettear/loggo"
|
||||
)
|
||||
|
||||
// pipe data from a stream to 'feh'
|
||||
func streamToFeh(stream io.ReadCloser) error {
|
||||
timestamp := time.Now().UnixMilli()
|
||||
defer stream.Close()
|
||||
cmd := exec.Command("feh", "--no-fehbg", "--bg-fill", "-")
|
||||
stdin, stdinError := cmd.StdinPipe()
|
||||
if stdinError != nil {
|
||||
return stdinError
|
||||
}
|
||||
defer stdin.Close()
|
||||
stdout, stdoutError := cmd.StdoutPipe()
|
||||
if stdoutError != nil {
|
||||
return stdoutError
|
||||
}
|
||||
defer stdout.Close()
|
||||
stderr, stderrError := cmd.StderrPipe()
|
||||
if stderrError != nil {
|
||||
return stderrError
|
||||
}
|
||||
defer stderr.Close()
|
||||
cmd.Start()
|
||||
written, copyError := io.Copy(stdin, stream)
|
||||
if copyError != nil {
|
||||
return copyError
|
||||
}
|
||||
if written == 0 {
|
||||
_, stdoutError = io.ReadAll(stdout)
|
||||
if stdoutError != nil {
|
||||
return stdoutError
|
||||
}
|
||||
errorBytes, stderrError := io.ReadAll(stderr)
|
||||
if stderrError != nil {
|
||||
return stderrError
|
||||
}
|
||||
error := strings.TrimSpace(string(errorBytes))
|
||||
if len(error) > 0 {
|
||||
return errors.New(error)
|
||||
}
|
||||
}
|
||||
stdin.Close()
|
||||
cmd.Wait()
|
||||
loggo.DebugTimed("successfully piped the data stream to 'feh''", timestamp, "size: "+FormatBytes(written))
|
||||
return nil
|
||||
}
|
||||
|
||||
// write data from a stream to a file
|
||||
func streamToFile(stream io.ReadCloser, path string) error {
|
||||
timestamp := time.Now().UnixMilli()
|
||||
defer stream.Close()
|
||||
file, error := os.Create(path)
|
||||
if error != nil {
|
||||
return error
|
||||
}
|
||||
written, error := io.Copy(file, stream)
|
||||
if error != nil {
|
||||
return error
|
||||
}
|
||||
loggo.DebugTimed("successfully piped the data stream to file '"+path+"'", timestamp, "size: "+FormatBytes(written))
|
||||
return nil
|
||||
}
|
10
main.go
10
main.go
|
@ -1,17 +1,23 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.velvettear.de/velvettear/loggo"
|
||||
"git.velvettear.de/velvettear/slideshow/internal"
|
||||
"git.velvettear.de/velvettear/slideshow/internal/config"
|
||||
"git.velvettear.de/velvettear/slideshow/internal/slideshow"
|
||||
"git.velvettear.de/velvettear/slideshow/scanner"
|
||||
)
|
||||
|
||||
func main() {
|
||||
timestamp := time.Now().UnixMilli()
|
||||
loggo.Info("slideshow is starting now...")
|
||||
var waitgroup sync.WaitGroup
|
||||
waitgroup.Add(1)
|
||||
config.Initialize()
|
||||
internal.StartSlideshow()
|
||||
scanner.Initialize()
|
||||
slideshow.Start()
|
||||
waitgroup.Wait()
|
||||
loggo.InfoTimed("slideshow is shutting down now!", timestamp)
|
||||
}
|
||||
|
|
66
scanner/scanner.go
Normal file
66
scanner/scanner.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
package scanner
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"math/rand"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.velvettear.de/velvettear/loggo"
|
||||
"git.velvettear.de/velvettear/slideshow/internal/config"
|
||||
)
|
||||
|
||||
// slice of images
|
||||
var images []string
|
||||
|
||||
// temporary slice of images
|
||||
var tmpImages []string
|
||||
|
||||
// scan the specified directories for images
|
||||
func Initialize() {
|
||||
directory := config.Directory
|
||||
scan(directory)
|
||||
}
|
||||
|
||||
// get a random image
|
||||
func GetRandomImage() string {
|
||||
return images[rand.Intn(len(images))]
|
||||
}
|
||||
|
||||
// scan the specified directory
|
||||
func scan(directory string) {
|
||||
timestamp := time.Now().UnixMilli()
|
||||
loggo.Info("scanning directory for images and subdirectories...", "interval: "+strconv.FormatFloat(config.ScanInterval.Seconds(), 'f', 0, 64)+" seconds", "directory: "+directory)
|
||||
filepath.WalkDir(directory, checkDirectory)
|
||||
images = tmpImages
|
||||
tmpImages = nil
|
||||
loggo.InfoTimed("found "+strconv.Itoa(len(images))+" image(s)", timestamp)
|
||||
go scheduleRescan(directory)
|
||||
}
|
||||
|
||||
// sleep the specified interval and then trigger a rescan of the specified directory
|
||||
func scheduleRescan(directory string) {
|
||||
loggo.Debug("sleeping for " + strconv.FormatInt(config.ScanInterval.Milliseconds(), 10) + "ms before next scan...")
|
||||
time.Sleep(config.ScanInterval)
|
||||
scan(directory)
|
||||
}
|
||||
|
||||
// add image files to the slice of images and subdirectoies to the watcher
|
||||
func checkDirectory(path string, dir fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if dir.IsDir() || !isImage(path) {
|
||||
return nil
|
||||
}
|
||||
tmpImages = append(tmpImages, path)
|
||||
loggo.Debug("added image to temporary slice of images", path)
|
||||
return nil
|
||||
}
|
||||
|
||||
// check if a file is an image
|
||||
func isImage(file string) bool {
|
||||
return strings.HasSuffix(file, ".jpeg") || strings.HasSuffix(file, ".jpg") || strings.HasSuffix(file, ".png")
|
||||
}
|
|
@ -5,17 +5,14 @@ Description=slideshow
|
|||
Type=simple
|
||||
User=velvettear
|
||||
Environment="DISPLAY=:0"
|
||||
ENVIRONMENT="SLIDESHOW_API=localhost:3000"
|
||||
Environment="SLIDESHOW_INTERVAL=60"
|
||||
Environment="SLIDESHOW_RESOLUTION="
|
||||
Environment="SLIDESHOW_DIRECTORY=$HOME"
|
||||
Environment="SLIDESHOW_SCANINTERVAL=60"
|
||||
Environment="SLIDESHOW_RESOLUTION=1920x1080"
|
||||
Environment="SLIDESHOW_LOGLEVEL=info"
|
||||
Environment="SLIDESHOW_PALETTE=/tmp/.slideshow.palette"
|
||||
Environment="SLIDESHOW_PALETTE_ALGORITHM="
|
||||
Environment="SLIDESHOW_PALETTE_COLORS="
|
||||
Environment="SLIDESHOW_SCRIPT="
|
||||
Environment="SLIDESHOW_SCRIPT_ARGS="
|
||||
Environment="SLIDESHOW_SCRIPT_ASYNC="
|
||||
Environment="SLIDESHOW_SCRIPT_STAGE="
|
||||
Environment="SLIDESHOW_LOGLEVEL=debug"
|
||||
Environment="SLIDESHOW_PALETTE_ALGORITHM=wsm"
|
||||
Environment="SLIDESHOW_PALETTE_COLORS=16"
|
||||
ExecStart=/opt/slideshow/slideshow
|
||||
|
||||
[Install]
|
||||
|
|
Loading…
Reference in a new issue