complete rewrite of the project; now server-client based
This commit is contained in:
parent
5eb3fd0ea1
commit
36ef2274c5
14 changed files with 436 additions and 485 deletions
32
.vscode/launch.json
vendored
32
.vscode/launch.json
vendored
|
@ -2,38 +2,22 @@
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "slideshow-scaled",
|
"name": "slideshow",
|
||||||
"type": "go",
|
"type": "go",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"mode": "auto",
|
"mode": "auto",
|
||||||
"program": "${workspaceFolder}/main.go",
|
"program": "${workspaceFolder}/main.go",
|
||||||
"env":{
|
"env":{
|
||||||
"SLIDESHOW_INTERVAL": "10",
|
"SLIDESHOW_API": "localhost:3000",
|
||||||
"SLIDESHOW_DIRECTORY": "/home/velvettear/images",
|
"SLIDESHOW_INTERVAL": "60",
|
||||||
"SLIDESHOW_SCANINTERVAL": "10",
|
|
||||||
"SLIDESHOW_TMPFILE": "/tmp/.slideshow.img",
|
|
||||||
"SLIDESHOW_RESOLUTION": "",
|
"SLIDESHOW_RESOLUTION": "",
|
||||||
"SLIDESHOW_LOGLEVEL": "debug",
|
|
||||||
"SLIDESHOW_PALETTE": "/tmp/.slideshow.palette",
|
"SLIDESHOW_PALETTE": "/tmp/.slideshow.palette",
|
||||||
"SLIDESHOW_PALETTE_ALGORITHM": "wsm",
|
"SLIDESHOW_PALETTE_ALGORITHM": "",
|
||||||
"SLIDESHOW_PALETTE_COLORS": "32"
|
"SLIDESHOW_PALETTE_COLORS": "",
|
||||||
},
|
"SLIDESHOW_SCRIPT": "/tmp/cat-palette.sh",
|
||||||
"console": "integratedTerminal"
|
"SLIDESHOW_SCRIPT_ASYNC": "true",
|
||||||
},
|
"SLIDESHOW_SCRIPT_STAGE": "post_image",
|
||||||
{
|
|
||||||
"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_LOGLEVEL": "debug",
|
||||||
"SLIDESHOW_PALETTE": "/tmp/.slideshow.palette",
|
|
||||||
"SLIDESHOW_PALETTE_ALGORITHM": "wsm",
|
|
||||||
"SLIDESHOW_PALETTE_COLORS": "16"
|
|
||||||
},
|
},
|
||||||
"console": "integratedTerminal"
|
"console": "integratedTerminal"
|
||||||
}
|
}
|
||||||
|
|
27
README.md
27
README.md
|
@ -1,11 +1,10 @@
|
||||||
# slideshow
|
# slideshow
|
||||||
|
|
||||||
a simple cli application to start a background image slideshow using 'feh'.
|
[slideshow-api](https://git.velvettear.de/velvettear/slideshow-api)'s client to set (scaled) images as background via 'feh' and retrieve color palettes.
|
||||||
|
|
||||||
## requirements
|
## requirements
|
||||||
|
|
||||||
- [feh](https://feh.finalrewind.org/)
|
- [feh](https://feh.finalrewind.org/)
|
||||||
- [ImageMagick](https://imagemagick.org/) (optional)
|
|
||||||
|
|
||||||
## configuration
|
## configuration
|
||||||
|
|
||||||
|
@ -13,21 +12,31 @@ configuration is entirely done via environment variables.
|
||||||
|
|
||||||
| variable | default | description |
|
| variable | default | description |
|
||||||
| --------------------------- | ----------------- | -----------------------------------------------------------|
|
| --------------------------- | ----------------- | -----------------------------------------------------------|
|
||||||
|
| SLIDESHOW_API | | the address of the slideshow-api server |
|
||||||
| SLIDESHOW_INTERVAL | 60 | the interval of the slideshow in seconds |
|
| 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_TMPFILE | | path to a temporary file |
|
|
||||||
| SLIDESHOW_RESOLUTION | | the resolution to which images are scaled (i.e. 1920x1080) |
|
| 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 | | 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_ALGORITHM | "wsm" | the algorithm used to generate the color palette |
|
||||||
| SLIDESHOW_PALETTE_COLORS | 16 | the amount of colors generated |
|
| SLIDESHOW_PALETTE_COLORS | 16 | the amount of colors generated |
|
||||||
|
| SLIDESHOW_SCRIPT | | path to a script to execute after each loop |
|
||||||
|
| 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 |
|
| SLIDESHOW_LOGLEVEL | "info" | the log level |
|
||||||
|
|
||||||
**note:**
|
**note:**
|
||||||
- if `SLIDESHOW_RESOLUTION` is unset the images will be displayed as they are (without any scaling).
|
|
||||||
- if `SLIDESHOW_TMPFILE` is set a temporary file will be used instead of keeping the image internally buffered. this could be useful on systems with limited ram.
|
|
||||||
|
|
||||||
**available log levels:**
|
- 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:
|
||||||
|
|
||||||
- `debug`
|
- `debug`
|
||||||
- `info`
|
- `info`
|
||||||
|
@ -42,4 +51,4 @@ configuration is entirely done via environment variables.
|
||||||
- `wsm`
|
- `wsm`
|
||||||
- `wu`
|
- `wu`
|
||||||
|
|
||||||
for more information regarding the color palette see [color-thief](https://github.com/kennykarnama/color-thief).
|
for more information regarding the color palette see [color-thief](https://github.com/kennykarnama/color-thief).
|
4
go.sum
4
go.sum
|
@ -2,8 +2,6 @@ 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=
|
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 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
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 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
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=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
@ -13,3 +11,5 @@ 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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
|
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.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=
|
||||||
|
|
|
@ -2,6 +2,7 @@ package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -9,51 +10,38 @@ import (
|
||||||
"git.velvettear.de/velvettear/loggo"
|
"git.velvettear.de/velvettear/loggo"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var ApiAddress string
|
||||||
var Interval time.Duration
|
var Interval time.Duration
|
||||||
var Directory string
|
|
||||||
var ScanInterval time.Duration
|
|
||||||
var TmpFile string
|
|
||||||
var Resolution string
|
var Resolution string
|
||||||
var PaletteFile string
|
var PaletteFile string
|
||||||
var PaletteAlgorithm int
|
var PaletteAlgorithm string
|
||||||
var PaletteColors int
|
var PaletteColors int
|
||||||
|
var Script string
|
||||||
|
var ScriptAsync bool
|
||||||
|
var ScriptStage string
|
||||||
|
|
||||||
// initialize the config
|
// initialize the config
|
||||||
func Initialize() {
|
func Initialize() {
|
||||||
loggo.SetLogLevelByName(os.Getenv("SLIDESHOW_LOGLEVEL"))
|
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"))
|
tmpInt, _ := strconv.Atoi(os.Getenv("SLIDESHOW_INTERVAL"))
|
||||||
if tmpInt <= 0 {
|
if tmpInt <= 0 {
|
||||||
tmpInt = 60
|
tmpInt = 60
|
||||||
}
|
}
|
||||||
Interval = time.Duration(tmpInt) * time.Second
|
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
|
|
||||||
TmpFile = os.Getenv("SLIDESHOW_TMPFILE")
|
|
||||||
Resolution = os.Getenv("SLIDESHOW_RESOLUTION")
|
Resolution = os.Getenv("SLIDESHOW_RESOLUTION")
|
||||||
if len(Resolution) > 0 {
|
if len(Resolution) > 0 {
|
||||||
width, height, found := strings.Cut(Resolution, "x")
|
width, height, found := strings.Cut(Resolution, "x")
|
||||||
if !found {
|
if !found {
|
||||||
loggo.Fatal("encountered an error parsing the configured resolution, make sure to specify the format like '1920x1080'")
|
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 {
|
if error != nil {
|
||||||
loggo.Fatal("encountered an error parsing the configured width '" + width + "'")
|
loggo.Fatal("encountered an error parsing the configured width '" + width + "'")
|
||||||
}
|
}
|
||||||
|
@ -63,17 +51,66 @@ func Initialize() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PaletteFile = os.Getenv("SLIDESHOW_PALETTE")
|
PaletteFile = os.Getenv("SLIDESHOW_PALETTE")
|
||||||
tmpString := os.Getenv("SLIDESHOW_PALETTE_ALGORITHM")
|
PaletteAlgorithm = os.Getenv("SLIDESHOW_PALETTE_ALGORITHM")
|
||||||
if strings.ToLower(tmpString) == "wu" {
|
|
||||||
PaletteAlgorithm = 0
|
|
||||||
} else {
|
|
||||||
PaletteAlgorithm = 1
|
|
||||||
}
|
|
||||||
tmpInt, _ = strconv.Atoi(os.Getenv("SLIDESHOW_PALETTE_COLORS"))
|
tmpInt, _ = strconv.Atoi(os.Getenv("SLIDESHOW_PALETTE_COLORS"))
|
||||||
if tmpInt <= 0 {
|
if tmpInt <= 0 {
|
||||||
tmpInt = 16
|
tmpInt = 16
|
||||||
}
|
}
|
||||||
PaletteColors = tmpInt
|
PaletteColors = tmpInt
|
||||||
|
Script = os.Getenv("SLIDESHOW_SCRIPT")
|
||||||
|
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
|
// check if a resolution has been specified
|
||||||
|
@ -86,7 +123,32 @@ func IsPaletteSet() bool {
|
||||||
return len(PaletteFile) > 0
|
return len(PaletteFile) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if a temporary file has been specified
|
// check if a script has been specified
|
||||||
func IsTemporaryFileSet() bool {
|
func IsScriptSet() bool {
|
||||||
return len(TmpFile) > 0
|
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"
|
||||||
}
|
}
|
||||||
|
|
17
internal/formatter.go
Normal file
17
internal/formatter.go
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
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
|
||||||
|
}
|
101
internal/requests.go
Normal file
101
internal/requests.go
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
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
|
||||||
|
}
|
115
internal/slideshow.go
Normal file
115
internal/slideshow.go
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
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()
|
||||||
|
if !config.IsPaletteSet() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer waitgroup.Done()
|
||||||
|
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)
|
||||||
|
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...)
|
||||||
|
}
|
|
@ -1,86 +0,0 @@
|
||||||
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))
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
package slideshow
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.velvettear.de/velvettear/loggo"
|
|
||||||
"git.velvettear.de/velvettear/slideshow/internal/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
// scale an image
|
|
||||||
func scale(image string, output string) ([]byte, error) {
|
|
||||||
timestamp := time.Now().UnixMilli()
|
|
||||||
if len(output) == 0 {
|
|
||||||
output = "-"
|
|
||||||
}
|
|
||||||
cmd := exec.Command("convert", image, "-resize", config.Resolution+"^", "-gravity", "center", "-extent", config.Resolution, output)
|
|
||||||
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)
|
|
||||||
return data, nil
|
|
||||||
}
|
|
|
@ -1,188 +0,0 @@
|
||||||
package slideshow
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"image/color"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"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 loopTime time.Duration
|
|
||||||
for {
|
|
||||||
loopTimestamp := time.Now()
|
|
||||||
for !scanner.HasFoundImages() {
|
|
||||||
sleepTime := time.Until(scanner.GetNextScan().Add(scanner.GetLastScanDuration()))
|
|
||||||
if sleepTime <= 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
loggo.Info("there have no images been found yet, waiting for " + strconv.FormatInt(sleepTime.Milliseconds(), 10) + "ms...")
|
|
||||||
time.Sleep(sleepTime)
|
|
||||||
}
|
|
||||||
var image string
|
|
||||||
var palette []color.Color
|
|
||||||
var data []byte
|
|
||||||
for {
|
|
||||||
image = scanner.GetRandomImage()
|
|
||||||
if image != previousImage {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if config.IsResolutionSet() {
|
|
||||||
tmp, error := scale(image, config.TmpFile)
|
|
||||||
if error != nil {
|
|
||||||
loggo.Error("encountered an error scaling an image", "image: "+image, error.Error())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
data = tmp
|
|
||||||
} else {
|
|
||||||
if !config.IsTemporaryFileSet() {
|
|
||||||
tmp, error := os.ReadFile(image)
|
|
||||||
if error != nil {
|
|
||||||
loggo.Error("encountered an error reading an image", "image: "+image, error.Error())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
data = tmp
|
|
||||||
} else {
|
|
||||||
file, error := os.Open(image)
|
|
||||||
if error != nil {
|
|
||||||
loggo.Error("encountered an error opening an image", "image: "+image, error.Error())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
tmpFile, error := os.Create(config.TmpFile)
|
|
||||||
if error != nil {
|
|
||||||
loggo.Error("encountered an error opening the temporary file", "path: "+config.TmpFile, error.Error())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
defer tmpFile.Close()
|
|
||||||
var errorMessage string
|
|
||||||
buffer := make([]byte, 4096)
|
|
||||||
for {
|
|
||||||
read, error := file.Read(buffer)
|
|
||||||
if error != nil && error != io.EOF {
|
|
||||||
errorMessage = error.Error()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if read == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
_, error = tmpFile.Write(buffer[:read])
|
|
||||||
if error != nil {
|
|
||||||
errorMessage = error.Error()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(errorMessage) > 0 {
|
|
||||||
loggo.Error("encountered an error reading an image", "image: "+image, errorMessage)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if config.IsTemporaryFileSet() {
|
|
||||||
palette, _ = getColorPalette(config.TmpFile)
|
|
||||||
} else {
|
|
||||||
palette, _ = getColorPaletteRaw(data)
|
|
||||||
}
|
|
||||||
loopTime = time.Since(loopTimestamp)
|
|
||||||
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)
|
|
||||||
var err error
|
|
||||||
if config.IsTemporaryFileSet() {
|
|
||||||
err = setBackgroundImage(config.TmpFile)
|
|
||||||
} else {
|
|
||||||
err = setBackground(data)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
loggo.Error("encountered an error setting the background image", err.Error())
|
|
||||||
}
|
|
||||||
loggo.Info("set new background image", "image: "+image)
|
|
||||||
sleepTime = config.Interval - loopTime
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// pipe data to 'feh' via stdin to set the background image
|
|
||||||
func setBackground(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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
73
internal/streams.go
Normal file
73
internal/streams.go
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
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
|
||||||
|
}
|
6
main.go
6
main.go
|
@ -4,16 +4,14 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.velvettear.de/velvettear/loggo"
|
"git.velvettear.de/velvettear/loggo"
|
||||||
|
"git.velvettear.de/velvettear/slideshow/internal"
|
||||||
"git.velvettear.de/velvettear/slideshow/internal/config"
|
"git.velvettear.de/velvettear/slideshow/internal/config"
|
||||||
"git.velvettear.de/velvettear/slideshow/internal/slideshow"
|
|
||||||
"git.velvettear.de/velvettear/slideshow/scanner"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
timestamp := time.Now().UnixMilli()
|
timestamp := time.Now().UnixMilli()
|
||||||
loggo.Info("slideshow is starting now...")
|
loggo.Info("slideshow is starting now...")
|
||||||
config.Initialize()
|
config.Initialize()
|
||||||
scanner.Initialize()
|
internal.StartSlideshow()
|
||||||
slideshow.Start()
|
|
||||||
loggo.InfoTimed("slideshow is shutting down now!", timestamp)
|
loggo.InfoTimed("slideshow is shutting down now!", timestamp)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,90 +0,0 @@
|
||||||
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
|
|
||||||
|
|
||||||
// time of next scan
|
|
||||||
var nextScan time.Time
|
|
||||||
|
|
||||||
// duration of the last scan
|
|
||||||
var lastScanDuration time.Duration
|
|
||||||
|
|
||||||
// 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))]
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if images has been found
|
|
||||||
func HasFoundImages() bool {
|
|
||||||
return len(images) > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the time of the next scan
|
|
||||||
func GetNextScan() time.Time {
|
|
||||||
return nextScan
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the duration of the last scan
|
|
||||||
func GetLastScanDuration() time.Duration {
|
|
||||||
return lastScanDuration
|
|
||||||
}
|
|
||||||
|
|
||||||
// scan the specified directory
|
|
||||||
func scan(directory string) {
|
|
||||||
timestamp := time.Now()
|
|
||||||
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.UnixMilli())
|
|
||||||
lastScanDuration = time.Since(timestamp)
|
|
||||||
nextScan = time.Now().Add(config.ScanInterval)
|
|
||||||
go scheduleRescan(directory, nextScan)
|
|
||||||
}
|
|
||||||
|
|
||||||
// sleep the specified interval and then trigger a rescan of the specified directory
|
|
||||||
func scheduleRescan(directory string, timestamp time.Time) {
|
|
||||||
sleepTime := time.Until(timestamp)
|
|
||||||
loggo.Debug("sleeping for " + strconv.FormatInt(sleepTime.Milliseconds(), 10) + "ms before next scan...")
|
|
||||||
time.Sleep(sleepTime)
|
|
||||||
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")
|
|
||||||
}
|
|
|
@ -9,10 +9,12 @@ Environment="SLIDESHOW_INTERVAL=60"
|
||||||
Environment="SLIDESHOW_DIRECTORY=$HOME"
|
Environment="SLIDESHOW_DIRECTORY=$HOME"
|
||||||
Environment="SLIDESHOW_SCANINTERVAL=60"
|
Environment="SLIDESHOW_SCANINTERVAL=60"
|
||||||
Environment="SLIDESHOW_RESOLUTION=1920x1080"
|
Environment="SLIDESHOW_RESOLUTION=1920x1080"
|
||||||
Environment="SLIDESHOW_LOGLEVEL=info"
|
|
||||||
Environment="SLIDESHOW_PALETTE=/tmp/.slideshow.palette"
|
Environment="SLIDESHOW_PALETTE=/tmp/.slideshow.palette"
|
||||||
Environment="SLIDESHOW_PALETTE_ALGORITHM=wsm"
|
Environment="SLIDESHOW_PALETTE_ALGORITHM=wsm"
|
||||||
Environment="SLIDESHOW_PALETTE_COLORS=16"
|
Environment="SLIDESHOW_PALETTE_COLORS=16"
|
||||||
|
Environment="SLIDESHOW_SCRIPT="
|
||||||
|
Environment="SLIDESHOW_SCRIPT_ASYNC=true"
|
||||||
|
Environment="SLIDESHOW_LOGLEVEL=info"
|
||||||
ExecStart=/opt/slideshow/slideshow
|
ExecStart=/opt/slideshow/slideshow
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
|
|
Loading…
Reference in a new issue