From 6927d54cbd61ba3cf809440d544a8a2bb4379741 Mon Sep 17 00:00:00 2001 From: ari melody Date: Fri, 12 Jun 2026 04:50:45 +0100 Subject: [PATCH] =?UTF-8?q?first=20commit!=20=F0=9F=8E=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit port "ari is learning!" assets to live go backend. some prep work for further developments for ari melody LIVE --- .gitignore | 1 + broadcast/broadcast.go | 74 +++++++++++++++++ go.mod | 37 +++++++++ go.sum | 78 ++++++++++++++++++ learning/learning.go | 167 ++++++++++++++++++++++++++++++++++++++ learning/pages/brb.html | 27 ++++++ learning/pages/bye.html | 27 ++++++ learning/pages/index.html | 27 ++++++ learning/public/cards.css | 125 ++++++++++++++++++++++++++++ learning/public/cards.js | 51 ++++++++++++ main.go | 60 ++++++++++++++ mpris/mpris.go | 1 + 12 files changed, 675 insertions(+) create mode 100644 .gitignore create mode 100644 broadcast/broadcast.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 learning/learning.go create mode 100644 learning/pages/brb.html create mode 100644 learning/pages/bye.html create mode 100644 learning/pages/index.html create mode 100644 learning/public/cards.css create mode 100644 learning/public/cards.js create mode 100644 main.go create mode 100644 mpris/mpris.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ec14f66 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +learning-title.txt diff --git a/broadcast/broadcast.go b/broadcast/broadcast.go new file mode 100644 index 0000000..6ac98fc --- /dev/null +++ b/broadcast/broadcast.go @@ -0,0 +1,74 @@ +package broadcast + +import "context" + +type BroadcastChannel[T comparable] interface { + Subscribe() <-chan T + Cancel(<-chan T) +} + +type broadcastChannel[T comparable] struct { + source <-chan T + listeners []chan T + addListener chan chan T + removeListener chan (<-chan T) +} + +func (b *broadcastChannel[T]) Subscribe() <-chan T { + newListener := make(chan T) + b.addListener <- newListener + return newListener +} + +func (b *broadcastChannel[T]) Cancel(channel <-chan T) { + b.removeListener <- channel +} + +func NewBroadcastChannel[T comparable](ctx context.Context, source <-chan T) BroadcastChannel[T] { + service := &broadcastChannel[T]{ + source: source, + listeners: make([]chan T, 0), + addListener: make(chan chan T), + removeListener: make(chan (<-chan T)), + } + go service.serve(ctx) + return service +} + +func (b *broadcastChannel[T]) serve(ctx context.Context) { + defer func() { + for _, listener := range b.listeners { + if listener != nil { + close(listener) + } + } + }() + + for { + select { + case <-ctx.Done(): + return + case newListener := <-b.addListener: + b.listeners = append(b.listeners, newListener) + case removeListener := <-b.removeListener: + for i, ch := range b.listeners { + if ch == removeListener { + b.listeners[i] = b.listeners[len(b.listeners) - 1] + b.listeners = b.listeners[:len(b.listeners) - 1] + close(ch) + break + } + } + case val, ok := <-b.source: + if !ok { return } + for _, listener := range b.listeners { + if listener == nil { continue } + select { + case listener <- val: + case <-ctx.Done(): + return + } + } + } + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7dd510e --- /dev/null +++ b/go.mod @@ -0,0 +1,37 @@ +module codeberg.org/arimelody/ari-stream-utils + +go 1.26.4 + +require github.com/gin-gonic/gin v1.12.0 + +require ( + github.com/bytedance/gopkg v0.1.3 // indirect + github.com/bytedance/sonic v1.15.0 // indirect + github.com/bytedance/sonic/loader v0.5.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/gabriel-vasile/mimetype v1.4.12 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.30.1 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-yaml v1.19.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.59.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.1 // indirect + go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect + golang.org/x/arch v0.22.0 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/net v0.51.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..6a9c680 --- /dev/null +++ b/go.sum @@ -0,0 +1,78 @@ +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= +github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= +github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= +github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= +github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= +github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= +github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +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= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= +github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= +github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= +go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= +golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= +golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/learning/learning.go b/learning/learning.go new file mode 100644 index 0000000..139ed45 --- /dev/null +++ b/learning/learning.go @@ -0,0 +1,167 @@ +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(): + } +} diff --git a/learning/pages/brb.html b/learning/pages/brb.html new file mode 100644 index 0000000..acec234 --- /dev/null +++ b/learning/pages/brb.html @@ -0,0 +1,27 @@ + + + + + + brb - ari is LEARNING + + + +
+
+

today's show:

+

stream.place

+
+
+

💫 ari is LEARNING

+

be right back! ⏳

+
+
+

@arimelody.space

+

{time}

+
+
+ +
+ + diff --git a/learning/pages/bye.html b/learning/pages/bye.html new file mode 100644 index 0000000..8947d86 --- /dev/null +++ b/learning/pages/bye.html @@ -0,0 +1,27 @@ + + + + + + bye - ari is LEARNING + + + +
+
+

wrapping up...

+

stream.place

+
+
+

💫 ari is LEARNING

+

see you around! 👋

+
+
+

@arimelody.space

+

{time}

+
+
+ +
+ + diff --git a/learning/pages/index.html b/learning/pages/index.html new file mode 100644 index 0000000..33b113a --- /dev/null +++ b/learning/pages/index.html @@ -0,0 +1,27 @@ + + + + + + starting soon... - ari is LEARNING + + + +
+
+

today's show:

+

stream.place

+
+
+

💫 ari is LEARNING

+

starting soon... 💚

+
+
+

@arimelody.space

+

{time}

+
+
+ +
+ + diff --git a/learning/public/cards.css b/learning/public/cards.css new file mode 100644 index 0000000..25e3ee9 --- /dev/null +++ b/learning/public/cards.css @@ -0,0 +1,125 @@ +:root { + --bg-0: #e0e0e0; + --on-bg-0: #303030; + --on-bg-1: #606060; + --on-bg-2: #808080; +} + +@media (prefers-color-scheme: dark) { + :root { + --bg-0: #202020; + --on-bg-0: #e0e0e0; + --on-bg-1: #c0c0c0; + --on-bg-2: #808080; + } +} + +body { + margin: 0; + padding: 0; + + width: 100%; + height: 100vh; + + font-family: "Inter", "Arial", sans-serif; + font-size: 32px; + + color: var(--on-bg-0); + background-color: var(--bg-0); + + transition: color 1s, background-color 1s; +} + +#app { + width: 100%; + height: 100%; + + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.vignette { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100vh; + background-image: radial-gradient(transparent, black); + opacity: .1; + pointer-events: none; +} + +.title { + display: flex; + flex-direction: column; + justify-content: center; + align-items: end; +} + +.title h1, .title h2 { + margin: 0; +} + +.title h1 { + font-size: 2em; + margin-bottom: -.2em; +} + +.title h2 { + font-size: 1.25em; + color: var(--on-bg-2); + animation: pulse .5s alternate ease-in-out infinite; +} + +@keyframes pulse { + from { opacity: 1; } + to { opacity: .5; } +} + +header { + position: fixed; + top: 0; +} + +footer { + position: fixed; + bottom: 0; +} + +header, +footer { + width: calc(100vw - 2.8em); + padding: 1em 1.4em; + display: flex; + flex-direction: row; + justify-content: space-between; + + color: var(--on-bg-2); +} + +header h3, +footer h3 { + margin: 0; + font-family: "Monaspace Argon", monospace; +} + +#time { + font-family: "Monaspace Argon", monospace; +} + +#cover { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: var(--bg-0); + transition: opacity 1s ease-out; +} + +#cover.hidden { + opacity: 0; + pointer-events: none; +} diff --git a/learning/public/cards.js b/learning/public/cards.js new file mode 100644 index 0000000..7f83c83 --- /dev/null +++ b/learning/public/cards.js @@ -0,0 +1,51 @@ +const timeEl = document.getElementById("time"); +const titleEl = document.getElementById("title"); +const coverEl = document.getElementById("cover"); + +function render() { + const date = new Date(); + + const tick = (date.getMilliseconds() * 0.001 % 1.0 > 0.5) ? ":" : " "; + + const hh = `${date.getUTCHours()}`.padStart(2, "0"); + const mm = `${date.getUTCMinutes()}`.padStart(2, "0"); + const ss = `${date.getUTCSeconds()}`.padStart(2, "0"); + timeEl.innerText = `${hh}${tick}${mm}${tick}${ss}`; + + requestAnimationFrame(render); +} + +function ready() { + coverEl.classList.add("hidden"); + + requestAnimationFrame(render); +} + +async function updateTitle() { + if (!titleEl) { + ready(); + return; + } + + const eventSource = new EventSource("/learning/sse"); + eventSource.addEventListener("open", () => { + console.log("Connected."); + + eventSource.addEventListener("title-update", event => { + console.log(`title updated: \"${titleEl.innerText}\" -> \"${event.data}\"`) + titleEl.innerText = event.data; + ready(); + }); + + eventSource.addEventListener("error", () => { + eventSource.removeEventListener("error"); + eventSource.close(); + console.error("Connection lost, or an error has occurred."); + console.log("Attempting to reconnect..."); + setTimeout(() => { + updateTitle(); + }, 1000); + }); + }); +} +updateTitle(); diff --git a/main.go b/main.go new file mode 100644 index 0000000..c0ce017 --- /dev/null +++ b/main.go @@ -0,0 +1,60 @@ +package main + +import ( + "context" + "fmt" + "log" + "math" + "os" + "os/signal" + "strconv" + "syscall" + + "codeberg.org/arimelody/ari-stream-utils/learning" + "github.com/gin-gonic/gin" +) + +var DEFAULT_HOST string = "0.0.0.0" +var DEFAULT_PORT int16 = 8080 + +func main() { + failed := make(chan error) + ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer cancel() + + gin.SetMode(gin.ReleaseMode) + + host := DEFAULT_HOST + port := DEFAULT_PORT + + if len(os.Args) > 1 { + _port, err := strconv.Atoi(os.Args[1]) + if _port > math.MaxUint16 { + log.Fatalf("Port must be between 1 and 65535: %d", _port) + } + port = int16(_port) + if err != nil { + log.Fatalf("Failed to parse port: %v", err) + } + } + + learningService := learning.New(ctx, learning.ServiceConfig{ + TitleFilePath: "learning-title.txt", + }) + + srv := gin.Default() + learningService.BindRoutes(srv.Group("/learning")) + + go learningService.Run(ctx) + go func() { + log.Printf("Now serving at http://%s:%d\n", host, port) + failed <- srv.Run(fmt.Sprintf("%s:%d", host, port)) + }() + + select { + case err := <-failed: + log.Fatal(err) + case <-ctx.Done(): + log.Print("Shutting down...") + } +} diff --git a/mpris/mpris.go b/mpris/mpris.go new file mode 100644 index 0000000..31d5fb9 --- /dev/null +++ b/mpris/mpris.go @@ -0,0 +1 @@ +package mpris