commit cf3715068c81e9a3a8b6ebd4a6a3dc298f819011 Author: Iris Lightshard Date: Tue Oct 29 23:38:05 2024 -0600 grimoire: icecast frontend diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8be27a0 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/grimoire diff --git a/config.go b/config.go new file mode 100644 index 0000000..041235a --- /dev/null +++ b/config.go @@ -0,0 +1,112 @@ +package main + +import ( + "os" + "path/filepath" + "runtime" + "strings" +) + +type Config struct { + Streams map[string]string // stream in the form stream::name=url + DataDir string // location of templates/ and static/ + Name string // name of this instance + ListenAddress string // address the webserver listens on +} + +func GetConfigLocation() string { + home := os.Getenv("HOME") + appdata := os.Getenv("APPDATA") + switch runtime.GOOS { + case "windows": + return filepath.Join(appdata, "grimoire") + case "darwin": + return filepath.Join(home, "Library", "Application Support", "grimoire") + case "plan9": + return filepath.Join(home, "lib", "grimoire") + default: + return filepath.Join(home, ".config", "grimoire") + } +} + +func ensureConfigLocationExists() { + fileInfo, err := os.Stat(GetConfigLocation()) + + if os.IsNotExist(err) { + os.MkdirAll(GetConfigLocation(), os.ModePerm) + } else if !fileInfo.IsDir() { + panic("Config location is not a directory!") + } +} + +func ReadConfig() *Config { + ensureConfigLocationExists() + return parseConfig(filepath.Join(GetConfigLocation(), "grimoire.conf")) +} + +func (self *Config) Write() error { + ensureConfigLocationExists() + return writeConfig(self, filepath.Join(GetConfigLocation(), "grimoire.conf")) +} + +func writeConfig(cfg *Config, configFile string) error { + f, err := os.Create(configFile) + if err != nil { + return err + } + + defer f.Close() + + f.WriteString("listenAddress=" + cfg.ListenAddress + "\n") + f.WriteString("dataDir=" + cfg.DataDir + "\n") + f.WriteString("name=" + cfg.Name + "\n") + for k, v := range cfg.Streams { + f.WriteString("stream::" + k + "=" + v) + } + return nil +} + +func parseConfig(configFile string) *Config { + f, err := os.ReadFile(configFile) + cfg := &Config{} + cfg.Streams = make(map[string]string) + if err != nil { + return cfg + } + + fileData := string(f[:]) + + lines := strings.Split(fileData, "\n") + + for _, l := range lines { + if len(l) == 0 { + continue + } + if !strings.Contains(l, "=") { + panic("Malformed config not in INI format") + } + + kvp := strings.Split(l, "=") + k := strings.TrimSpace(kvp[0]) + v := strings.TrimSpace(kvp[1]) + switch k { + case "listenAddress": + cfg.ListenAddress = v + case "dataDir": + cfg.DataDir = v + case "name": + cfg.Name = v + default: + if strings.HasPrefix(k, "stream::") { + stream := strings.Split(k, "stream::")[1] + if len(stream) == 0 { + panic("stream in config has no name!") + } + cfg.Streams[stream] = v + } else { + panic("Unrecognized config option: " + k) + } + } + } + return cfg +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..eef5640 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module forge.lightcrystal.systems/lightcrystal/grimoire + +go 1.23.0 + +require hacklab.nilfm.cc/quartzgun v0.3.2 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..be61828 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +hacklab.nilfm.cc/quartzgun v0.3.2 h1:PmRFZ/IgsXVWyNn1iOsQ/ZeMnOQIQy0PzFakhXBdZoU= +hacklab.nilfm.cc/quartzgun v0.3.2/go.mod h1:P6qK4HB0CD/xfyRq8wdEGevAPFDDmv0KCaESSvv93LU= diff --git a/grimoire.go b/grimoire.go new file mode 100644 index 0000000..8b13bee --- /dev/null +++ b/grimoire.go @@ -0,0 +1,60 @@ +package main + +import ( + "hacklab.nilfm.cc/quartzgun/renderer" + "hacklab.nilfm.cc/quartzgun/router" + "hacklab.nilfm.cc/quartzgun/util" + + "net/http" + "path/filepath" + "html/template" +) + +func withTitleAndStreamsAndSentry(next http.Handler, cfg Config, sentry *Sentry) http.Handler { + handlerFunc := func(w http.ResponseWriter, req *http.Request) { + util.AddContextValue(req, "title", cfg.Name) + util.AddContextValue(req, "streams", cfg.Streams) + util.AddContextValue(req, "sentry", sentry) + next.ServeHTTP(w, req) + } + + return http.HandlerFunc(handlerFunc) +} + +type Sentry struct {} + +func (self *Sentry) GetStatus(url string) int { + resp, err := http.Get(url) + if err != nil { + return 500 + } else { + return resp.StatusCode + } +} + +func main() { + cfg := ReadConfig() + + templateRoot := filepath.Join(cfg.DataDir, "templates") + + rtr := &router.Router{ + StaticPaths: map[string]string{ + "/static/": filepath.Join(cfg.DataDir, "static"), + }, + Fallback: *template.Must(template.ParseFiles( + filepath.Join(templateRoot, "err.html"), + )), + } + + + + rtr.Get("/", withTitleAndStreamsAndSentry( + renderer.Template( + filepath.Join(templateRoot, "radio.html"), + ), + *cfg, + &Sentry{}, + )) + + http.ListenAndServe(cfg.ListenAddress, rtr) +} \ No newline at end of file diff --git a/static/banner.gif b/static/banner.gif new file mode 100644 index 0000000..f85a9ce Binary files /dev/null and b/static/banner.gif differ diff --git a/static/online.gif b/static/online.gif new file mode 100644 index 0000000..ab77e84 Binary files /dev/null and b/static/online.gif differ diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..73068a1 --- /dev/null +++ b/static/style.css @@ -0,0 +1,61 @@ +* { + padding: 0; + margin: 0; +} + +body { + background:#000; + color:#fff; + text-align:center; + font-family: monospace; +} + +header { + background:url('/static/banner.gif'); + background-position:center; + background-size:cover; + width: 100vw; + padding: 2ch; + box-sizing: border-box; + margin: auto; + box-shadow: inset 0 0 0.5em 1em black; +} + +h1, li a { + color: goldenrod; + text-shadow: black 0 0 1em; + font-weight: bold; + text-decoration: none; +} + +h1 { + color: #fff !important; +} + +ul { list-style: none;padding:0;margin: 0.5em; } + +ul li { + background-position:center; + background-size:cover; + box-shadow: inset 0 0 0.5em 1em black; + max-width: fit-content; + margin: auto; +} + +ul li.online { + background-image:url('/static/online.gif'); + +} + +ul li.offline a { + color: grey; +} + +ul li.offline a::after { + content: " (offline)" +} + +ul li a { + display:inline-block; + padding: 2em; +} \ No newline at end of file diff --git a/templates/err.html b/templates/err.html new file mode 100644 index 0000000..b47a6ac --- /dev/null +++ b/templates/err.html @@ -0,0 +1,18 @@ +{{ $params := (.Context).Value "params" }} + + + + + + + + + Grimoire — Error + + +

Error

+ {{$params.ErrorCode}}: {{$params.ErrorMessage}} + + + + diff --git a/templates/radio.html b/templates/radio.html new file mode 100644 index 0000000..eaf2195 --- /dev/null +++ b/templates/radio.html @@ -0,0 +1,30 @@ +{{ $title := (.Context).Value "title" }} +{{ $stations := (.Context).Value "streams" }} +{{ $sentry := (.Context).Value "sentry" }} + + + + + + + + + {{ $title }} + + +

{{$title}}

+
+ +
+ + + +