Files
IRHM ac8d8769af import: Support games importing
Fixing watcharr import from watcharr export not importing users games.

Also works if user edits the table in import/process to change the type of something to a game (and multi results works too).

Added warning message if user tries importing a watcharr export that has games when `!serverFeatures.games`
2026-03-09 03:34:57 +00:00

310 lines
16 KiB
Go
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Watcharr is licensed under the GPLv3 license.
package main
import (
"bufio"
"fmt"
"log"
"log/slog"
"net/http"
"net/http/httputil"
"os"
"os/exec"
"path"
"time"
_ "embed"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/validator/v10"
"github.com/joho/godotenv"
"github.com/sbondCo/Watcharr/config"
"github.com/sbondCo/Watcharr/database"
"github.com/sbondCo/Watcharr/database/entity"
"github.com/sbondCo/Watcharr/domain"
"github.com/sbondCo/Watcharr/feature/activity"
"github.com/sbondCo/Watcharr/feature/arr"
"github.com/sbondCo/Watcharr/feature/auth"
"github.com/sbondCo/Watcharr/feature/content"
"github.com/sbondCo/Watcharr/feature/discover"
"github.com/sbondCo/Watcharr/feature/feature"
"github.com/sbondCo/Watcharr/feature/follow"
"github.com/sbondCo/Watcharr/feature/game"
"github.com/sbondCo/Watcharr/feature/imprt"
"github.com/sbondCo/Watcharr/feature/jellyfin"
"github.com/sbondCo/Watcharr/feature/job"
"github.com/sbondCo/Watcharr/feature/plex"
"github.com/sbondCo/Watcharr/feature/profile"
"github.com/sbondCo/Watcharr/feature/search"
"github.com/sbondCo/Watcharr/feature/server"
"github.com/sbondCo/Watcharr/feature/setup"
"github.com/sbondCo/Watcharr/feature/tag"
"github.com/sbondCo/Watcharr/feature/task"
"github.com/sbondCo/Watcharr/feature/user"
"github.com/sbondCo/Watcharr/feature/watched"
"github.com/sbondCo/Watcharr/feature/watched/episode"
"github.com/sbondCo/Watcharr/feature/watched/season"
"github.com/sbondCo/Watcharr/logging"
"github.com/sbondCo/Watcharr/media/tmdb"
"github.com/sbondCo/Watcharr/router"
taskl "github.com/sbondCo/Watcharr/task"
)
//go:embed VERSION
var version string
func main() {
err := godotenv.Load()
if err != nil {
// Do not fail if file does not exist
if !os.IsNotExist(err) {
log.Fatal("Failed to load vars from .env file:", err)
}
}
multiw := logging.Setup(path.Join(config.DataPath, "watcharr.log"))
slog.Info("Watcharr Starting", "version", version)
fmt.Printf(`
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣀⣀⣀⣀⣀⣀⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣠⣴⣶⣿⠿⠛⠛⠛⠻⠿⣿⣿⣿⣿⣿⣶⣤⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣴⣿⣿⣿⣿⣿⠀⠀⠀⠀⠀⠀⠀⢀⣿⣿⣿⣿⣿⣷⣻⠶⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠄⠂⠀⢀⣠⣾⣿⣿⣿⣿⣿⣿⣿⡄⠀⠀⠀⢀⣤⣾⣿⣿⣿⣿⣿⣿⡿⣽⣻⣳⢎⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠄⢡⠂⠄⣢⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⣶⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣟⡷⣯⡞⣝⢆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠐⠀⠁⡐⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣳⣟⡾⣹⢎⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠠⢀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠂⣼⣿⣿⣿⣿⡿⠿⠛⠋⠉⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠛⠻⠿⣿⣿⣿⣿⣿⡿⣾⣝⣧⢻⡜⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢂⠐⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡀⠂⢸⣿⡿⠟⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⠻⠿⣿⣳⢯⣞⡳⣎⠅⠀⠀⠀⠀⠀⠀⠀⠀⠀⠠⢈⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠄⠁⠚⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠁⠛⢯⡞⣵⣋⠆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠁⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⠱⣍⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⡞⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡄⢀⣾⡇⠀⣾⣇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⡟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⠁⣾⣿⡇⢰⣿⣿⠀⠀⣆⠀⠀⠀⠀⢰⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⢰⣿⠁⠀⠀⠀⠀⠀⠀⠀⠀⡀⠀⠀⣼⡏⢰⣿⣿⠇⣾⣿⣿⡆⠀⣿⠀⠀⠀⠀⢸⣿⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠈⡇⠀⠀⠀⠀⠀⠀⠀⠀⠰⠃⠀⠒⠛⠃⠚⠿⣿⢰⣿⣿⣿⡇⣤⣿⣤⣶⣦⣀⢼⣿⣧⠀⢰⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⢠⣶⢰⣿⣿⣿⣧⡹⢓⣾⣾⣿⣿⣿⣧⣿⣿⣿⣿⣋⣁⣀⣀⣀⣁⠘⠃⢀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣾⡟⢋⠁⡀⠀⠉⠙⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠱⣚⣭⡿⢿⣿⣷⣦⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⡄⢠⣆⠀⠀⠀⠀⣿⣏⡀⣾⠀⠀⠀⠀⣰⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡏⣁⠀⢠⠀⠀⠉⠻⢿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⢠⢇⣾⣿⣷⠀⠀⠀⣿⣿⣿⣞⡓⠥⠬⣒⣷⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢿⠀⠀⠀⠀⠀⣦⠈⢳⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⢸⣾⣿⣿⣿⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣮⡢⢄⡀⠤⠾⢧⣦⣼⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⡇⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢟⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣶⣶⣶⣿⣿⡿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⣿⢁⣿⣿⠇⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣏⢾⡅⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠆⣼⣿⣿⣦⣾⠀⠀⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣾⣷⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠀⠀⠀⠀⠀⢀⠰⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⣼⣻⢿⣯⡿⣟⠇⠀⡜⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠇⠀⠀⠀⠀⠀⠌⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⣰⢧⡟⡿⣾⡽⢏⣿⣾⣿⡌⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣛⣻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠐⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⢀⡰⣣⢻⡜⣯⢳⡝⣼⣿⣿⣿⣿⣆⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡀⢂⠐⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⢠⠎⡵⢣⢧⡹⣜⢣⣿⣿⣿⣿⣿⣿⣿⣷⡌⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠋⠀⠀⠀⠀⠀⠀⠀⢀⠂⠔⡀⢂⠐⡀⢂⠠⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠡⢚⠴⣉⠦⡑⢎⢣⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⣙⠿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⡩⠂⠀⠀⠀⠀⠀⣀⡔⢦⠃⢈⠐⡀⢂⠐⠠⠀⠄⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠁⠎⡰⢡⠙⡌⣸⣿⣿⣿⣿⣿⣿⣿⣿⠿⠿⠟⠒⠌⠻⢿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠛⠉⠀⠈⠀⠀⠀⠀⠀⣀⠶⡱⢎⢧⢋⠀⡐⢀⠂⠌⢀⠂⢀⠂⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠁⠢⠑⡨⣟⠿⠟⠟⠋⠋⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠛⠛⠟⠛⠋⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢴⡩⢞⡱⢫⠜⡪⢅⠀⠂⠄⠂⠠⠀⠂⢀⠐⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢢⡙⢦⡙⡔⢣⠈⢀⠂⠈⡀⠐⠀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢤⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠂⠴⢉⠆⡁⠀⡀⠁⢀⠐⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠐⠡⠀⠀⠐⠀⠀⠀⠈⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠂⠈⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
██╗ ██╗ █████╗ ████████╗ ██████╗██╗ ██╗ █████╗ ██████╗ ██████╗
██║ ██║██╔══██╗╚══██╔══╝██╔════╝██║ ██║██╔══██╗██╔══██╗██╔══██╗
██║ █╗ ██║███████║ ██║ ██║ ███████║███████║██████╔╝██████╔╝
██║███╗██║██╔══██║ ██║ ██║ ██╔══██║██╔══██║██╔══██╗██╔══██╗
╚███╔███╔╝██║ ██║ ██║ ╚██████╗██║ ██║██║ ██║██║ ██║██║ ██║
╚══╝╚══╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝
Layer:%s Starting now. Get ready!
Thank you for running my (spaghetti) code on your system.`+"\n\n", version)
// Ensure data dir exists
err = ensureDirExists(config.DataPath)
if err != nil {
log.Fatal("Failed to create data dir:", err)
}
cfg, err := config.Get()
if err != nil {
log.Fatal("Failed to get server config!", err)
}
logging.SetLevel(cfg.DEBUG)
// Check if we want to be in DEV or PROD
isProd := true
if os.Getenv("MODE") == "DEV" {
slog.Info("Starting in DEV mode")
isProd = false
}
db, err := database.New()
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
if isProd {
go runUI()
gin.SetMode(gin.ReleaseMode)
}
gin.DefaultWriter = multiw
gine := gin.Default()
// Register our custom validators
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
v.RegisterValidation("validsearchtype", domain.ValidSearchType)
v.RegisterValidation("validdiscoverfilter", domain.ValidDiscoverFilter)
}
gine.Use(cors.New(cors.Config{
AllowOrigins: []string{"*"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{
"Content-Type",
"Content-Length",
"Accept-Encoding",
"X-CSRF-Token",
"Authorization",
"accept",
"origin",
"Cache-Control",
"X-Requested-With",
},
ExposeHeaders: []string{
"Content-Length",
"watcharr-lastviewedseason-saved",
},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
}))
if isProd {
// Proxy NoRoute requests to UI server
gine.NoRoute(func(c *gin.Context) {
director := func(req *http.Request) {
req.URL.Scheme = "http"
req.URL.Host = "127.0.0.1:3000"
}
proxy := &httputil.ReverseProxy{Director: director}
proxy.ServeHTTP(c.Writer, c.Request)
})
}
api := gine.Group("/api")
br := router.NewBaseRouter(db, api, cfg)
t := tmdb.NewTMDB(cfg.TMDB_KEY)
plexService := plex.NewService(cfg)
authService := auth.NewService(db, cfg, plexService)
authTrustedHeaderService := auth.NewTrustedHeaderService(db, cfg, authService)
contentService := content.NewService(db, t)
activityService := activity.NewService(db)
userService := user.NewService(db)
userManageService := user.NewManageService(db)
gameService := game.NewService(db, &br.Cfg.TWITCH, activityService)
watchedService := watched.NewService(db, contentService, gameService, activityService)
watchedSeasonService := season.NewService(db, activityService)
watchedEpisodeService := episode.NewService(
db,
watchedService,
watchedSeasonService,
contentService,
activityService,
userService)
jellyfinService := jellyfin.NewService(cfg)
jellyfinSyncService := jellyfin.NewSyncService(
cfg,
jellyfinService,
watchedService,
watchedSeasonService,
watchedEpisodeService,
activityService)
plexSyncService := plex.NewSyncService(
plexService,
watchedService,
watchedSeasonService,
watchedEpisodeService,
activityService)
featureService := feature.NewService(cfg)
profileService := profile.NewService(db)
followService := follow.NewService(db)
tagService := tag.NewService(db, watchedService)
searchService := search.NewService(db, br.Cfg, contentService, watchedService)
discoverService := discover.NewService(db, br.Cfg, contentService)
importService := imprt.NewService(
db,
watchedService,
watchedSeasonService,
watchedEpisodeService,
contentService,
activityService,
tagService,
searchService)
importTraktService := imprt.NewTraktService(importService)
auth.NewRouter(br, authService, authTrustedHeaderService).AddRoutes()
content.NewRouter(br, contentService, watchedService).AddRoutes()
watched.NewRouter(br, t, watchedService).AddRoutes()
season.NewRouter(br, watchedSeasonService).AddRoutes()
episode.NewRouter(br, watchedEpisodeService).AddRoutes()
activity.NewRouter(br, activityService).AddRoutes()
profile.NewRouter(br, profileService).AddRoutes()
jellyfin.NewRouter(br, jellyfinService, jellyfinSyncService).AddRoutes()
plex.NewRouter(br, plexSyncService).AddRoutes()
user.NewRouter(br, userService, userManageService).AddRoutes()
follow.NewRouter(br, followService).AddRoutes()
imprt.NewRouter(br, importService, importTraktService).AddRoutes()
server.NewRouter(br, plexService, authTrustedHeaderService, userManageService).AddRoutes()
feature.NewRouter(br, featureService).AddRoutes()
arr.NewRouter(br, contentService).AddRoutes()
job.NewRouter(br).AddRoutes()
task.NewRouter(br).AddRoutes()
tag.NewRouter(br, tagService).AddRoutes()
game.NewRouter(br, gameService, watchedService).AddRoutes()
search.NewRouter(br, searchService, watchedService).AddRoutes()
discover.NewRouter(br, discoverService, watchedService).AddRoutes()
// Only add setup routes if there are no users found in db.
var userCount int64
if uresp := db.Model(&entity.User{}).Count(&userCount); uresp.Error == nil {
if userCount != 0 {
slog.Debug("registered users found.. skipped creating setup routes.")
} else {
slog.Info("No users found.. creating setup routes.")
setup.NewRouter(br, authService).AddRoutes()
}
} else {
slog.Error("Failed to check if any users exist.. not registering setup routes",
"error", uresp.Error)
}
api.Static("/img", path.Join(config.DataPath, "img"))
go taskl.SetupTasks(cfg, db)
gine.Run("0.0.0.0:3080")
}
// Run UI server
func runUI() {
cmd := exec.Command("node", "ui/index.js")
cmdReader, err := cmd.StdoutPipe()
if err != nil {
log.Fatal("UI ERR @ get stdout read pipe: ", err)
}
scanner := bufio.NewScanner(cmdReader)
go func() {
for scanner.Scan() {
fmt.Println(scanner.Text())
}
}()
if err := cmd.Start(); err != nil {
log.Fatal("UI ERR @ command start: ", err)
}
if err := cmd.Wait(); err != nil {
log.Fatal("UI ERR @ command wait: ", err)
}
}
func ensureDirExists(dir string) error {
err := os.MkdirAll(dir, 0764)
if err != nil {
return err
}
return nil
}