stream-tools/learning/learning.go
ari melody 6927d54cbd
first commit! 🎉
port "ari is learning!" assets to live go backend.
some prep work for further developments for ari melody LIVE
2026-06-12 04:50:45 +01:00

167 lines
4.1 KiB
Go

package learning
import (
"context"
"embed"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
"sync"
"time"
"codeberg.org/arimelody/ari-is-learning/broadcast"
"github.com/gin-gonic/gin"
)
//go:embed public
var publicFS embed.FS
//go:embed pages
var pagesFS embed.FS
type (
ServiceConfig struct {
TitleFilePath string
}
Service struct {
cfg ServiceConfig
ctx context.Context
titleFileMutex sync.Mutex
titleLastModTime time.Time
titleText string
titleUpdated chan string
titleUpdatedBroadcast broadcast.BroadcastChannel[string]
}
)
func New(ctx context.Context, cfg ServiceConfig) *Service {
titleUpdated := make(chan string)
if _, err := os.Stat(cfg.TitleFilePath); os.IsNotExist(err) {
log.Printf("Learning title file [%s] does not exist, creating one...", cfg.TitleFilePath)
os.WriteFile(cfg.TitleFilePath, []byte("Untitled"), 0644)
} else if err != nil {
log.Fatalf("Failed to stat title file: %v", err)
}
srv := &Service{
cfg: cfg,
ctx: ctx,
titleFileMutex: sync.Mutex{},
titleLastModTime: time.Unix(0, 0),
titleText: "",
titleUpdated: titleUpdated,
titleUpdatedBroadcast: broadcast.NewBroadcastChannel(ctx, titleUpdated),
}
return srv
}
func (srv *Service) BindRoutes(group *gin.RouterGroup) {
group.GET("/public/*path", func(ctx *gin.Context) {
path := strings.TrimPrefix(ctx.Request.URL.Path, "/learning/")
http.ServeFileFS(ctx.Writer, ctx.Request, publicFS, path)
})
group.GET("/sse", func(ctx *gin.Context) {
ctx.Header("connection", "keep-alive")
ctx.SSEvent("title-update", srv.titleText)
titleUpdated := srv.titleUpdatedBroadcast.Subscribe()
ticker := time.NewTicker(10 * time.Millisecond)
ctx.Stream(func(w io.Writer) bool {
select {
case titleText := <-titleUpdated:
ctx.SSEvent("title-update", titleText)
case <-ticker.C:
}
return true
})
srv.titleUpdatedBroadcast.Cancel(titleUpdated)
})
group.POST("/title", func(ctx *gin.Context) {
data, err := io.ReadAll(ctx.Request.Body)
if err != nil {
log.Printf("Failed to read request body: %v", err)
ctx.String(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
return
}
log.Printf("Waiting on title file lock...")
srv.titleFileMutex.Lock()
defer srv.titleFileMutex.Unlock()
log.Printf("Got it!")
file, err := os.OpenFile(srv.cfg.TitleFilePath, os.O_RDWR | os.O_CREATE | os.O_TRUNC, 0)
if err != nil {
log.Printf("Failed to open title file: %v", err)
ctx.String(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
return
}
if _, err := file.Write(data); err != nil {
log.Printf("Failed to write to title file: %v", err)
ctx.String(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
return
}
ctx.String(http.StatusOK, http.StatusText(http.StatusOK))
})
group.GET("/hello", func(ctx *gin.Context) {
http.ServeFileFS(ctx.Writer, ctx.Request, pagesFS, "pages/index.html")
})
group.GET("/brb", func(ctx *gin.Context) {
http.ServeFileFS(ctx.Writer, ctx.Request, pagesFS, "pages/brb.html")
})
group.GET("/bye", func(ctx *gin.Context) {
http.ServeFileFS(ctx.Writer, ctx.Request, pagesFS, "pages/bye.html")
})
}
func (srv *Service) Run(ctx context.Context) {
failed := make(chan error)
go func() {
for {
srv.titleFileMutex.Lock()
stat, err := os.Stat(srv.cfg.TitleFilePath)
if err != nil {
failed<-fmt.Errorf("Failed to stat title file: %v", err)
return
}
if srv.titleLastModTime.Before(stat.ModTime()) {
data, err := os.ReadFile(srv.cfg.TitleFilePath)
if err != nil {
failed<-fmt.Errorf("Failed to read title file: %v", err)
return
}
log.Printf(
"title updated: \"%s\" -> \"%s\"",
srv.titleText,
strings.TrimSpace(string(data)),
)
srv.titleText = strings.TrimSpace(string(data))
if !srv.titleLastModTime.IsZero() { srv.titleUpdated<-srv.titleText }
srv.titleLastModTime = stat.ModTime()
}
srv.titleFileMutex.Unlock()
time.Sleep(100 * time.Millisecond)
}
}()
select {
case err := <-failed:
log.Fatalf("Learning service error: %v", err)
case <-ctx.Done():
}
}