stream-tools/learning/learning.go

127 lines
3.3 KiB
Go

package learning
import (
"context"
"embed"
"io"
"log"
"net/http"
"os"
"strings"
"sync"
"time"
"codeberg.org/arimelody/ari-stream-tools/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
titleFileMutex sync.Mutex
titleLastModTime time.Time
titleText string
titleUpdated chan string
titleUpdatedBroadcast broadcast.BroadcastChannel[string]
}
)
func New(ctx context.Context, cfg ServiceConfig) *Service {
titleText := []byte{}
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)
titleText = []byte("Untitled")
} else if err != nil {
log.Fatalf("Failed to stat title file: %v", err)
} else {
titleText, err = os.ReadFile(cfg.TitleFilePath)
if err != nil { log.Fatalf("Failed to read title file: %v", err) }
}
srv := &Service{
cfg: cfg,
titleFileMutex: sync.Mutex{},
titleLastModTime: time.Unix(0, 0),
titleText: string(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
}
srv.titleFileMutex.Lock()
defer srv.titleFileMutex.Unlock()
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
}
srv.titleUpdated<-string(data)
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")
})
}