initial commit
This commit is contained in:
commit
bdaa3a97b6
14 changed files with 529 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
./slideshow-api
|
||||||
|
__debug_bin*
|
23
.vscode/launch.json
vendored
Normal file
23
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"version": "0.0.1",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "slideshow-api-scaled",
|
||||||
|
"type": "go",
|
||||||
|
"request": "launch",
|
||||||
|
"mode": "auto",
|
||||||
|
"program": "${workspaceFolder}/main.go",
|
||||||
|
"env":{
|
||||||
|
"SLIDESHOW_ADDRESS": "0.0.0.0",
|
||||||
|
"SLIDESHOW_PORT": "3000",
|
||||||
|
"SLIDESHOW_DIRECTORY": "/home/velvettear/images",
|
||||||
|
"SLIDESHOW_SCANINTERVAL": "10",
|
||||||
|
"SLIDESHOW_RESOLUTION": "600x1024",
|
||||||
|
"SLIDESHOW_LOGLEVEL": "debug",
|
||||||
|
"SLIDESHOW_PALETTE_ALGORITHM": "wsm",
|
||||||
|
"SLIDESHOW_PALETTE_COLORS": "16"
|
||||||
|
},
|
||||||
|
"console": "integratedTerminal"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
20
LICENSE.md
Normal file
20
LICENSE.md
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# MIT License
|
||||||
|
**Copyright (c) 2023 Daniel Sommer \<daniel.sommer@velvettear.de\>**
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is furnished
|
||||||
|
to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice (including the next
|
||||||
|
paragraph) shall be included in all copies or substantial portions of the
|
||||||
|
Software.
|
||||||
|
|
||||||
|
**THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
|
||||||
|
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
|
||||||
|
OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.**
|
42
README.md
Normal file
42
README.md
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
# slideshow
|
||||||
|
|
||||||
|
a simple web server serving (scaled) images and color palettes for [slideshow](https://git.velvettear.de/velvettear/slideshow).
|
||||||
|
|
||||||
|
## requirements
|
||||||
|
|
||||||
|
- [ImageMagick](https://imagemagick.org/) (optional)
|
||||||
|
|
||||||
|
## configuration
|
||||||
|
|
||||||
|
configuration is entirely done via environment variables.
|
||||||
|
|
||||||
|
| variable | default | description |
|
||||||
|
| --------------------------- | ----------------- | -----------------------------------------------------------|
|
||||||
|
| SLIDESHOW_ADDRESS | "0.0.0.0" | the listen address of the web server |
|
||||||
|
| SLIDESHOW_PORT | 3000 | the port of the web server |
|
||||||
|
| 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_ALGORITHM | "wsm" | the algorithm used to generate the color palette |
|
||||||
|
| SLIDESHOW_PALETTE_COLORS | 16 | the amount of colors generated |
|
||||||
|
| SLIDESHOW_LOGLEVEL | "info" | the log level |
|
||||||
|
|
||||||
|
**note:**
|
||||||
|
if `SLIDESHOW_RESOLUTION` is unset the images will served as they are (without any scaling).
|
||||||
|
|
||||||
|
**available log levels:**
|
||||||
|
|
||||||
|
- `debug`
|
||||||
|
- `info`
|
||||||
|
- `warning`
|
||||||
|
- `error`
|
||||||
|
- `fatal`
|
||||||
|
|
||||||
|
## color palette
|
||||||
|
|
||||||
|
**available algorithms:**
|
||||||
|
|
||||||
|
- `wsm`
|
||||||
|
- `wu`
|
||||||
|
|
||||||
|
for more information regarding the color palette see [color-thief](https://github.com/kennykarnama/color-thief).
|
13
go.mod
Normal file
13
go.mod
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
module git.velvettear.de/velvettear/slideshow
|
||||||
|
|
||||||
|
go 1.21
|
||||||
|
|
||||||
|
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.14.0 // indirect
|
||||||
|
)
|
15
go.sum
Normal file
15
go.sum
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
git.velvettear.de/velvettear/loggo v0.0.0-20231113084149-980a00b4e084 h1:13S20q+usZ+yJr2cxxOpnPrOd5+4PwCbQg56RuWSkKk=
|
||||||
|
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=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/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/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
64
internal/api/server.go
Normal file
64
internal/api/server.go
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"git.velvettear.de/velvettear/loggo"
|
||||||
|
"git.velvettear.de/velvettear/slideshow/internal"
|
||||||
|
"git.velvettear.de/velvettear/slideshow/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// start the web server
|
||||||
|
func Run() {
|
||||||
|
address := config.ServerAddress + ":" + strconv.Itoa(config.ServerPort)
|
||||||
|
loggo.Info("starting web server...", "address: "+address)
|
||||||
|
registerHandlers()
|
||||||
|
error := http.ListenAndServe(address, nil)
|
||||||
|
if error != nil {
|
||||||
|
loggo.Fatal("encountered an error starting the web server", "address: "+address, error.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// register the handlers
|
||||||
|
func registerHandlers() {
|
||||||
|
http.HandleFunc("/", handleImageRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle the request to '/image'
|
||||||
|
func handleImageRequest(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
if !internal.HasFoundImages() {
|
||||||
|
writer.WriteHeader(404)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
name, data, error := internal.GetRandomImage()
|
||||||
|
if error != nil {
|
||||||
|
loggo.Error("encountered an error getting a random image", error.Error())
|
||||||
|
writer.WriteHeader(501)
|
||||||
|
writer.Write([]byte("encountered an internal server error (" + error.Error() + ")\n"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
palette, error := internal.GetColorPalette(data)
|
||||||
|
if error != nil {
|
||||||
|
writer.WriteHeader(501)
|
||||||
|
writer.Write([]byte("encountered an internal server error (" + error.Error() + ")\n"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bytes, error := json.Marshal(struct {
|
||||||
|
Name string
|
||||||
|
Palette string
|
||||||
|
Data []byte
|
||||||
|
}{
|
||||||
|
name,
|
||||||
|
palette,
|
||||||
|
data,
|
||||||
|
})
|
||||||
|
if error != nil {
|
||||||
|
loggo.Error("encountered an error marshalling the json response", error.Error())
|
||||||
|
writer.WriteHeader(501)
|
||||||
|
writer.Write([]byte("encountered an internal server error (" + error.Error() + ")\n"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writer.Write(bytes)
|
||||||
|
}
|
44
internal/cache.go
Normal file
44
internal/cache.go
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// the internally pre buffered (and scaled) image
|
||||||
|
var cachedImage []byte
|
||||||
|
|
||||||
|
// the internally pre buffered color palette
|
||||||
|
var cachedPalette []byte
|
||||||
|
|
||||||
|
// cache an image
|
||||||
|
func cacheImage(image []byte) {
|
||||||
|
cachedImage = image
|
||||||
|
}
|
||||||
|
|
||||||
|
// cache a color palette
|
||||||
|
func cachePalette(colors []color.Color) {
|
||||||
|
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) + "\""
|
||||||
|
}
|
||||||
|
cachedPalette = []byte(palette)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if an image is cached
|
||||||
|
func hasCachedImage() bool {
|
||||||
|
return len(cachedImage) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if a color palette is cached
|
||||||
|
func hasCachedColorPalette() bool {
|
||||||
|
return len(cachedPalette) > 0
|
||||||
|
}
|
80
internal/config/config.go
Normal file
80
internal/config/config.go
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.velvettear.de/velvettear/loggo"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ServerAddress string
|
||||||
|
var ServerPort int
|
||||||
|
var Directory string
|
||||||
|
var ScanInterval time.Duration
|
||||||
|
var Resolution string
|
||||||
|
var PaletteAlgorithm int
|
||||||
|
var PaletteColors int
|
||||||
|
|
||||||
|
// initialize the config
|
||||||
|
func Initialize() {
|
||||||
|
loggo.SetLogLevelByName(os.Getenv("SLIDESHOW_LOGLEVEL"))
|
||||||
|
ServerAddress = os.Getenv("SLIDESHOW_ADDRESS")
|
||||||
|
tmpInt, _ := strconv.Atoi(os.Getenv("SLIDESHOW_PORT"))
|
||||||
|
if tmpInt <= 0 {
|
||||||
|
tmpInt = 3000
|
||||||
|
}
|
||||||
|
ServerPort = tmpInt
|
||||||
|
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)
|
||||||
|
if error != nil {
|
||||||
|
loggo.Fatal("encountered an error parsing the configured width '" + width + "'")
|
||||||
|
}
|
||||||
|
_, error = strconv.Atoi(height)
|
||||||
|
if error != nil {
|
||||||
|
loggo.Fatal("encountered an error parsing the configured height '" + height + "'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if a resolution has been specified
|
||||||
|
func IsResolutionSet() bool {
|
||||||
|
return len(Resolution) > 0
|
||||||
|
}
|
65
internal/palette.go
Normal file
65
internal/palette.go
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.velvettear.de/velvettear/loggo"
|
||||||
|
"git.velvettear.de/velvettear/slideshow/internal/config"
|
||||||
|
color_thief "github.com/kennykarnama/color-thief"
|
||||||
|
)
|
||||||
|
|
||||||
|
// extract the given amount of dominant colors of raw image bytes
|
||||||
|
func GetColorPalette(data []byte) (string, error) {
|
||||||
|
var palette string
|
||||||
|
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 palette, 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))
|
||||||
|
return palette, error
|
||||||
|
} else {
|
||||||
|
loggo.DebugTimed("generated color palette from raw image bytes", timestamp, "colors: "+strconv.Itoa(amount))
|
||||||
|
}
|
||||||
|
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) + "\""
|
||||||
|
}
|
||||||
|
return palette, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract the given amount of dominant colors of an image
|
||||||
|
func getColorPalette(image string) ([]color.Color, error) {
|
||||||
|
var colors []color.Color
|
||||||
|
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))
|
||||||
|
}
|
43
internal/scale.go
Normal file
43
internal/scale.go
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.velvettear.de/velvettear/loggo"
|
||||||
|
"git.velvettear.de/velvettear/slideshow/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// scale an image
|
||||||
|
func ScaleImage(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)
|
||||||
|
return data, nil
|
||||||
|
}
|
80
internal/scanner.go
Normal file
80
internal/scanner.go
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io/fs"
|
||||||
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
"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 directory
|
||||||
|
func Scan() {
|
||||||
|
timestamp := time.Now()
|
||||||
|
directory := config.Directory
|
||||||
|
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())
|
||||||
|
go scheduleRescan()
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if images has been found
|
||||||
|
func HasFoundImages() bool {
|
||||||
|
return len(images) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// get a random image
|
||||||
|
func GetRandomImage() (string, []byte, error) {
|
||||||
|
var name string
|
||||||
|
var data []byte
|
||||||
|
var error error
|
||||||
|
if len(images) == 0 {
|
||||||
|
return name, data, errors.New("no images have been found")
|
||||||
|
}
|
||||||
|
image := images[rand.Intn(len(images))]
|
||||||
|
if config.IsResolutionSet() {
|
||||||
|
data, error = ScaleImage(image)
|
||||||
|
} else {
|
||||||
|
data, error = os.ReadFile(image)
|
||||||
|
}
|
||||||
|
return image, data, error
|
||||||
|
}
|
||||||
|
|
||||||
|
// sleep the specified interval and then trigger a rescan of the specified directory
|
||||||
|
func scheduleRescan() {
|
||||||
|
loggo.Debug("sleeping for " + strconv.FormatInt(config.ScanInterval.Milliseconds(), 10) + "ms before next scan...")
|
||||||
|
time.Sleep(config.ScanInterval)
|
||||||
|
Scan()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
}
|
19
main.go
Normal file
19
main.go
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.velvettear.de/velvettear/loggo"
|
||||||
|
"git.velvettear.de/velvettear/slideshow/internal"
|
||||||
|
"git.velvettear.de/velvettear/slideshow/internal/api"
|
||||||
|
"git.velvettear.de/velvettear/slideshow/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
timestamp := time.Now().UnixMilli()
|
||||||
|
loggo.Info("slideshow-api is starting now...")
|
||||||
|
config.Initialize()
|
||||||
|
internal.Scan()
|
||||||
|
api.Run()
|
||||||
|
loggo.InfoTimed("slideshow-api is shutting down now!", timestamp)
|
||||||
|
}
|
19
slideshow-api.service
Normal file
19
slideshow-api.service
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
[Unit]
|
||||||
|
Description=slideshow-api
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=velvettear
|
||||||
|
Environment="DISPLAY=:0"
|
||||||
|
Environment="SLIDESHOW_ADDRESS=0.0.0.0"
|
||||||
|
Environment="SLIDESHOW_PORT=3000"
|
||||||
|
Environment="SLIDESHOW_DIRECTORY=$HOME"
|
||||||
|
Environment="SLIDESHOW_SCANINTERVAL=60"
|
||||||
|
Environment="SLIDESHOW_RESOLUTION="
|
||||||
|
Environment="SLIDESHOW_PALETTE_ALGORITHM=wsm"
|
||||||
|
Environment="SLIDESHOW_PALETTE_COLORS=16"
|
||||||
|
Environment="SLIDESHOW_LOGLEVEL=info"
|
||||||
|
ExecStart=/opt/slideshow-api/slideshow-api
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
Loading…
Reference in a new issue