2023-06-13 00:07:14 +03:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"embed"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"log"
|
2024-02-24 21:47:18 +02:00
|
|
|
"net"
|
2023-06-13 00:07:14 +03:00
|
|
|
"net/http"
|
|
|
|
|
|
|
|
_ "embed"
|
|
|
|
"flag"
|
|
|
|
"math/rand"
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/olahol/melody"
|
|
|
|
)
|
|
|
|
|
|
|
|
type PlayState = int
|
|
|
|
|
|
|
|
const (
|
|
|
|
NotPlaying PlayState = iota
|
|
|
|
PlayingFallback
|
|
|
|
PlayingVideo
|
|
|
|
)
|
|
|
|
|
|
|
|
type State struct {
|
|
|
|
VideoProcess ffmpegProcess
|
|
|
|
|
|
|
|
PlayState PlayState
|
|
|
|
CurrentFile string
|
|
|
|
Config
|
|
|
|
Websocket *melody.Melody
|
2024-02-24 21:47:18 +02:00
|
|
|
|
|
|
|
Users map[net.Addr]WebsocketUserInfo
|
|
|
|
}
|
|
|
|
|
|
|
|
type WebsocketUserInfo struct {
|
|
|
|
Username string
|
|
|
|
Color string
|
2023-06-13 00:07:14 +03:00
|
|
|
}
|
|
|
|
|
2024-02-25 01:12:33 +02:00
|
|
|
func (w WebsocketUserInfo) ToWebsocketUser() WebsocketUser {
|
|
|
|
return WebsocketUser{
|
|
|
|
Username: w.Username,
|
|
|
|
Color: w.Color,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-06-13 00:07:14 +03:00
|
|
|
type Config struct {
|
2024-02-25 21:08:04 +02:00
|
|
|
StreamCacheDir string
|
2023-06-13 00:07:14 +03:00
|
|
|
FallbackFile string
|
|
|
|
MediaFolder string
|
|
|
|
IgnoreHiddenFiles bool
|
|
|
|
|
2024-02-23 23:35:12 +02:00
|
|
|
PublicPort int
|
2024-02-23 19:42:04 +02:00
|
|
|
AdminApiPort int
|
2023-06-13 00:07:14 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *State) getRandomMediaFile() (foundFile string, err error) {
|
|
|
|
var files []string
|
|
|
|
files, err = listFilesRecursively(s.MediaFolder, s.IgnoreHiddenFiles)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
foundFile = files[rand.Intn(len(files))]
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func newState(c Config) (state State, err error) {
|
|
|
|
state = State{
|
|
|
|
Config: c,
|
|
|
|
PlayState: NotPlaying,
|
2024-02-24 21:47:18 +02:00
|
|
|
Users: make(map[net.Addr]WebsocketUserInfo),
|
2023-06-13 00:07:14 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
//go:embed html/*
|
|
|
|
var embedHTML embed.FS
|
|
|
|
|
|
|
|
var ErrMediaFolderNotSpecified = errors.New("Please specify media folder to use")
|
|
|
|
|
|
|
|
func parseFlags() (config Config, err error) {
|
2024-02-25 21:08:04 +02:00
|
|
|
flag.StringVar(&config.StreamCacheDir, "cache", "stream_cache", "location to dump hls/m3u8 livestream files")
|
2023-06-13 00:07:14 +03:00
|
|
|
flag.StringVar(&config.FallbackFile, "fallback", "family_guy_wheel.mp4", "Fallback video that is played when main stream is down")
|
2024-02-23 19:42:04 +02:00
|
|
|
flag.StringVar(&config.MediaFolder, "media", "", "Folder to play random media from")
|
|
|
|
flag.IntVar(&config.PublicPort, "public_port", 3000, "Port to run public website on")
|
|
|
|
flag.IntVar(&config.AdminApiPort, "admin_api_port", 3001, "Port to run admin api on")
|
2023-06-13 00:07:14 +03:00
|
|
|
flag.BoolVar(&config.IgnoreHiddenFiles, "hidden_files", true, "Ignores hidden files in random media picking")
|
|
|
|
flag.Parse()
|
|
|
|
|
|
|
|
if config.MediaFolder == "" {
|
|
|
|
err = ErrMediaFolderNotSpecified
|
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
config, err := parseFlags()
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
|
2024-02-25 21:08:04 +02:00
|
|
|
if err = RecreateDir(config.StreamCacheDir); err != nil {
|
2024-02-24 16:54:27 +02:00
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
|
2023-06-13 00:07:14 +03:00
|
|
|
state, err := newState(config)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
state.setupWebsocket()
|
|
|
|
|
|
|
|
if err = state.playFallback(); err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
public := gin.Default()
|
|
|
|
public.GET("/", func(c *gin.Context) {
|
|
|
|
ready, err := state.IsLivestreamReady()
|
|
|
|
if err != nil {
|
|
|
|
c.String(http.StatusInternalServerError, "Something went wrong")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if ready {
|
2024-02-25 00:08:06 +02:00
|
|
|
c.FileFromFS("html/stream.html", http.FS(embedHTML))
|
2023-06-13 00:07:14 +03:00
|
|
|
} else {
|
|
|
|
c.FileFromFS("html/loading.html", http.FS(embedHTML))
|
|
|
|
}
|
|
|
|
})
|
2024-02-25 00:08:06 +02:00
|
|
|
|
2023-06-13 00:07:14 +03:00
|
|
|
public.Static("/static", "static")
|
2024-02-25 21:08:04 +02:00
|
|
|
public.Static("/media", config.StreamCacheDir)
|
2023-06-13 00:07:14 +03:00
|
|
|
public.Any("/ws", func(c *gin.Context) {
|
|
|
|
state.Websocket.HandleRequest(c.Writer, c.Request)
|
|
|
|
})
|
|
|
|
|
|
|
|
publicAPI := public.Group("/api")
|
|
|
|
publicAPI.GET("now_playing", state.NowPlayingAPI)
|
|
|
|
publicAPI.GET("is_livestream_ready", state.IsLivestreamReadyAPI)
|
|
|
|
|
|
|
|
private := gin.Default()
|
|
|
|
|
|
|
|
// TODO: autoplay feature that keeps playing random things after current one ends
|
|
|
|
privateAPI := private.Group("/api")
|
|
|
|
privateAPI.GET("play", state.PlayAPI)
|
|
|
|
privateAPI.GET("random", state.PlayRandomAPI)
|
|
|
|
privateAPI.GET("stop", state.StopPlayingAPI)
|
|
|
|
privateAPI.GET("now_playing", state.NowPlayingAPI)
|
|
|
|
|
|
|
|
fmt.Println("Starting public server on port", fmt.Sprint(config.PublicPort))
|
|
|
|
go public.Run(fmt.Sprintf(":%d", config.PublicPort))
|
|
|
|
|
2024-02-23 19:42:04 +02:00
|
|
|
fmt.Println("Starting private server on port", fmt.Sprint(config.AdminApiPort))
|
|
|
|
log.Fatal(private.Run(fmt.Sprintf(":%d", config.AdminApiPort)))
|
2023-06-13 00:07:14 +03:00
|
|
|
}
|