init
Signed-off-by: Mirror <voice@magicalmirro.red>
This commit is contained in:
commit
45d48b78ea
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
videopage
|
||||||
|
web
|
||||||
|
config.yml
|
||||||
|
|
206
admin.go
Normal file
206
admin.go
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"crypto/subtle"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AdminApi struct {
|
||||||
|
mux *http.ServeMux
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAdminApi() (a *AdminApi) {
|
||||||
|
a = &AdminApi{}
|
||||||
|
a.mux = http.NewServeMux()
|
||||||
|
a.mux.HandleFunc("/", a.adminHandler)
|
||||||
|
a.mux.HandleFunc("/delete", a.deleteVideo)
|
||||||
|
a.mux.HandleFunc("/upload", a.uploadVideo)
|
||||||
|
a.mux.HandleFunc("/rename", a.renameVideo)
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
func (a *AdminApi) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if a.checkAuth(w, r) {
|
||||||
|
a.mux.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (a *AdminApi) checkAuth(w http.ResponseWriter, r *http.Request) (success bool) {
|
||||||
|
success = false
|
||||||
|
user, password, ok := r.BasicAuth()
|
||||||
|
if subtle.ConstantTimeCompare([]byte(user), []byte(config.AdminUser)) == 1 &&
|
||||||
|
subtle.ConstantTimeCompare([]byte(password), []byte(config.AdminPassword)) == 1 {
|
||||||
|
success = true
|
||||||
|
}
|
||||||
|
if !success {
|
||||||
|
if !ok {
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
}
|
||||||
|
w.Header().Set("WWW-Authenticate", fmt.Sprintf("Basic realm=\"%s\"", config.ApplicationName))
|
||||||
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
return success
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AdminApi) adminHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var v struct {
|
||||||
|
ApplicationName string
|
||||||
|
Videos []Video
|
||||||
|
}
|
||||||
|
v.ApplicationName = config.ApplicationName
|
||||||
|
|
||||||
|
dataDir := path.Join(config.DataDirectory, "video")
|
||||||
|
dir, err := os.ReadDir(dataDir)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("admin: can't read data directory: %s", err.Error())
|
||||||
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, entry := range dir {
|
||||||
|
if entry.Type().IsRegular() && strings.HasSuffix(entry.Name(), ".json") && entry.Name() != "index.json" {
|
||||||
|
video, err := readVideoInfo(path.Join(dataDir, entry.Name()))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("admin: can't read video info: %s", err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
v.Videos = append(v.Videos, video)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = executeTemplate(w, "admin.html", &v)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Can't execute template \"admin.html\": %s", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AdminApi) deleteVideo(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err := r.ParseForm()
|
||||||
|
referer := r.Header.Get("Referer")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("can't parse form: %s", err.Error())
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ID := r.Form.Get("fileid")
|
||||||
|
ID = path.Clean(path.Base(ID))
|
||||||
|
rawInfo, err := ioutil.ReadFile(path.Join(config.DataDirectory, "video", ID+".json"))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("can't read info: %s", err.Error())
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var info Video
|
||||||
|
err = json.Unmarshal(rawInfo, &info)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("can't read info: %s", err.Error())
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = os.Remove(path.Join(config.DataDirectory, "video", info.Name))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("can't remove video: %s", err.Error())
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = os.Remove(path.Join(config.DataDirectory, "video", ID+".json"))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("can't remove info: %s", err.Error())
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.Redirect(w, r, referer, http.StatusMovedPermanently)
|
||||||
|
}
|
||||||
|
func (a *AdminApi) uploadVideo(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err := r.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("can't parse form: %s", err.Error())
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
referer := r.Header.Get("Referer")
|
||||||
|
reader, err := r.MultipartReader()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("can't read multipart: %s", err.Error())
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
header, err := reader.NextPart()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
defer header.Close()
|
||||||
|
if header.FormName() != "file" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
filename := header.FileName()
|
||||||
|
if len(filename) == 0 {
|
||||||
|
http.Error(w, "Filename are empty", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tempFile, err := ioutil.TempFile(path.Join(config.DataDirectory, "tmp"), "upload-")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("can't create temp file: %s", err.Error())
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer tempFile.Close()
|
||||||
|
hashbucket := md5.New()
|
||||||
|
fileWriter := io.MultiWriter(tempFile, hashbucket)
|
||||||
|
written, err := io.Copy(fileWriter, header)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("can't write temp file: %s", err.Error())
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tmpName := path.Base(tempFile.Name())
|
||||||
|
err = tempFile.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("can't close file: %s", err.Error())
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var video Video
|
||||||
|
video.DisplayName = path.Base(path.Clean(filename))
|
||||||
|
video.ID = fmt.Sprintf("%x", hashbucket.Sum(nil))
|
||||||
|
video.Name = fmt.Sprintf("%s%s", video.ID, path.Ext(filename))
|
||||||
|
video.Size = written
|
||||||
|
err = os.Rename(path.Join(config.DataDirectory, "tmp", tmpName), path.Join(config.DataDirectory, "video", video.Name))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("can't rename file: %s", err.Error())
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
infoRaw, err := json.Marshal(&video)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("can't marshal info: %s", err.Error())
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = ioutil.WriteFile(path.Join(config.DataDirectory, "video", video.ID+".json"), infoRaw, os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("can't write info: %s", err.Error())
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
http.Redirect(w, r, referer, http.StatusMovedPermanently)
|
||||||
|
}
|
||||||
|
func (a *AdminApi) renameVideo(w http.ResponseWriter, r *http.Request) {}
|
42
config.go
Normal file
42
config.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
ApplicationName string `yaml:"application_name"`
|
||||||
|
ApplicationPrefix string `yaml:"application_prefix"`
|
||||||
|
DataDirectory string `yaml:"data_directory"`
|
||||||
|
ListenAddress string `yaml:"listen_address"`
|
||||||
|
AdminUser string `yaml:"admin_user"`
|
||||||
|
AdminPassword string `yaml:"admin_password"`
|
||||||
|
Generation int `yaml:"generation"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) ParseFile(path string) (err error) {
|
||||||
|
configRaw, err := ioutil.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("can't load configuration: %s", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = yaml.UnmarshalStrict(configRaw, c)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("can't parse configuration: %s", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfig() *Config {
|
||||||
|
return &Config{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfigParseFile(path string) (c *Config, err error) {
|
||||||
|
c = NewConfig()
|
||||||
|
err = c.ParseFile(path)
|
||||||
|
return
|
||||||
|
}
|
5
go.mod
Normal file
5
go.mod
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
module videopage
|
||||||
|
|
||||||
|
go 1.18
|
||||||
|
|
||||||
|
require gopkg.in/yaml.v2 v2.4.0 // indirect
|
3
go.sum
Normal file
3
go.sum
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
18
logger.go
Normal file
18
logger.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Logger struct {
|
||||||
|
h http.Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLogger(h http.Handler) (l *Logger) {
|
||||||
|
return &Logger{h: h}
|
||||||
|
}
|
||||||
|
func (l *Logger) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Printf("%s %s %s%s", r.RemoteAddr, r.Method, r.Host, r.URL)
|
||||||
|
l.h.ServeHTTP(w, r)
|
||||||
|
}
|
195
main.go
Normal file
195
main.go
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"crypto/md5"
|
||||||
|
"embed"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
templates *template.Template
|
||||||
|
//go:embed templates
|
||||||
|
fs embed.FS
|
||||||
|
config *Config
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var configPath string
|
||||||
|
var rescan bool
|
||||||
|
flag.StringVar(&configPath, "config", "config.yaml", "Path to the configuration file")
|
||||||
|
flag.BoolVar(&rescan, "rescan", false, "Rescan the data direcory")
|
||||||
|
flag.Parse()
|
||||||
|
var err error
|
||||||
|
config, err = NewConfigParseFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if rescan {
|
||||||
|
rescanFiles()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
entry, err := os.Stat(path.Join(config.DataDirectory, "tmp"))
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
err = os.Mkdir(path.Join(config.DataDirectory, "tmp"), os.ModeDir)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("FATAL: can't create tmp directory: %s", err.Error())
|
||||||
|
os.Exit(-1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("FATAL: can't stat tmp directory: %s", err.Error())
|
||||||
|
os.Exit(-2)
|
||||||
|
}
|
||||||
|
if !entry.IsDir() {
|
||||||
|
log.Printf("FATAL: tmp path isn't directory")
|
||||||
|
os.Exit(-3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templates = template.Must(template.ParseFS(fs, "templates/*"))
|
||||||
|
log.Printf("Loaded templates: %s", templates.DefinedTemplates())
|
||||||
|
videoFS := http.FileServer(http.Dir(config.DataDirectory))
|
||||||
|
adminMux := NewAdminApi()
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/", indexHandler)
|
||||||
|
mux.HandleFunc("/video/index.json", videoIndexHandler)
|
||||||
|
mux.Handle("/video/", videoFS)
|
||||||
|
mux.Handle("/admin/", http.StripPrefix("/admin", adminMux))
|
||||||
|
if len(config.ApplicationPrefix) > 0 {
|
||||||
|
http.ListenAndServe(config.ListenAddress, http.StripPrefix(config.ApplicationPrefix, NewLogger(mux)))
|
||||||
|
} else {
|
||||||
|
http.ListenAndServe(config.ListenAddress, NewLogger(mux))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func indexHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var v struct {
|
||||||
|
ApplicationName string
|
||||||
|
}
|
||||||
|
v.ApplicationName = config.ApplicationName
|
||||||
|
err := executeTemplate(w, "index.html", &v)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error executing temlate \"index.html\": %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func videoIndexHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var index VideoIndex
|
||||||
|
index.Success = true
|
||||||
|
index.Generation = config.Generation
|
||||||
|
dir, err := os.ReadDir(path.Join(config.DataDirectory, "video"))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("can't red video directory: %s", err.Error())
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, entry := range dir {
|
||||||
|
if entry.Type().IsRegular() && path.Ext(entry.Name()) == ".json" && entry.Name() != "index.json" {
|
||||||
|
var v Video
|
||||||
|
rawInfo, err := ioutil.ReadFile(path.Join(config.DataDirectory, "video", entry.Name()))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("can't read info for %s: %s", entry.Name(), err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(rawInfo, &v)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("can't read info for %s: %s", entry.Name(), err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
index.Videos = append(index.Videos, v)
|
||||||
|
index.Count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
indexRaw, err := json.Marshal(index)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("can't marshal index: %s", err.Error())
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write(indexRaw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func executeTemplate(w http.ResponseWriter, name string, data interface{}) (err error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
bufferRW := bufio.NewReadWriter(bufio.NewReader(&buf), bufio.NewWriter(&buf))
|
||||||
|
err = templates.ExecuteTemplate(bufferRW, name, data)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bufferRW.Flush()
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
io.Copy(w, bufferRW)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func rescanFiles() {
|
||||||
|
var index VideoIndex
|
||||||
|
index.Generation = config.Generation
|
||||||
|
index.Success = true
|
||||||
|
dirPath := path.Join(config.DataDirectory, "video")
|
||||||
|
dir, err := os.ReadDir(dirPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("can't read data directory: %s", err.Error())
|
||||||
|
}
|
||||||
|
for _, entry := range dir {
|
||||||
|
if entry.Type().IsRegular() {
|
||||||
|
if !strings.HasSuffix(entry.Name(), ".json") {
|
||||||
|
var v Video
|
||||||
|
file, err := os.Open(path.Join(dirPath, entry.Name()))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("can't read info file: %s", err.Error())
|
||||||
|
}
|
||||||
|
v.Name = entry.Name()
|
||||||
|
hashsum := md5.New()
|
||||||
|
_, err = io.Copy(hashsum, file)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("can't read info file: %s", err.Error())
|
||||||
|
}
|
||||||
|
hashstring := fmt.Sprintf("%x", hashsum.Sum(nil))
|
||||||
|
v.ID = hashstring
|
||||||
|
ext := path.Ext(entry.Name())
|
||||||
|
err = os.Rename(path.Join(dirPath, entry.Name()), path.Join(dirPath, string(hashstring)+ext))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("can't move file: %s", err.Error())
|
||||||
|
}
|
||||||
|
if info, _ := os.Stat(path.Join(dirPath, v.ID+".json")); info == nil {
|
||||||
|
rawInfo, err := json.Marshal(&v)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("can't write info: %s", err.Error())
|
||||||
|
}
|
||||||
|
err = ioutil.WriteFile(path.Join(dirPath, v.ID+".json"), rawInfo, os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("can't write info: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
index.Videos = append(index.Videos, v)
|
||||||
|
index.Count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rawIndex, err := json.Marshal(&index)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("can't create index: %s", err)
|
||||||
|
}
|
||||||
|
err = ioutil.WriteFile(path.Join(dirPath, "index.json"), rawIndex, os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("can't create index: %s", err)
|
||||||
|
}
|
||||||
|
}
|
41
templates/admin.html
Normal file
41
templates/admin.html
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
{{define "admin.html"}}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{{.ApplicationName}} | Admin page</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: black;
|
||||||
|
color: antiquewhite;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="playlist">
|
||||||
|
<p>Current playlist</p>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td>name</td>
|
||||||
|
<td>size</td>
|
||||||
|
<td>preview</td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{range .Videos}}
|
||||||
|
<tr>
|
||||||
|
<td><form action="delete" method="post"><input id="fileid" name="fileid" type="hidden" value="{{.ID}}"><input value="Delete" type="submit"></form></td>
|
||||||
|
<td>{{html .DisplayName}}</td>
|
||||||
|
<td>{{html .Size}}</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
<form action="upload" method="post" enctype="multipart/form-data">
|
||||||
|
<p>Add new: <input id="file" type="file" name="file" multiple> <input type="submit" ></p>
|
||||||
|
</form>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{end}}
|
100
templates/index.html
Normal file
100
templates/index.html
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
{{define "index.html"}}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{{.ApplicationName}}</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: black;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
.player-container .player {
|
||||||
|
object-fit: cover;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script type="text/javascript">
|
||||||
|
var player
|
||||||
|
var generation = 0
|
||||||
|
var current_track = -1
|
||||||
|
var total_tracks = -1
|
||||||
|
var track_list = []
|
||||||
|
var last_known_time = -1
|
||||||
|
options = {
|
||||||
|
timeout: 5000
|
||||||
|
}
|
||||||
|
function load() {
|
||||||
|
fetch("video/index.json", {timeout: 5000})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (!data.success) {
|
||||||
|
console.log("failed to fetch index.json")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (typeof data.generation =="number") {
|
||||||
|
if (generation == 0) {
|
||||||
|
generation = data.generation
|
||||||
|
}
|
||||||
|
if (data.generation > generation) {
|
||||||
|
location.reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
total_tracks = data.count
|
||||||
|
track_list = data.videos
|
||||||
|
if (current_track == -1) {
|
||||||
|
current_track = 0
|
||||||
|
player.src = "video/" + track_list[current_track].name
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
function videoEnd(){
|
||||||
|
if (total_tracks == -1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (player.ended) {
|
||||||
|
nextVideo()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
function nextVideo(){
|
||||||
|
current_track++
|
||||||
|
if (current_track > total_tracks-1) {
|
||||||
|
current_track = 0
|
||||||
|
}
|
||||||
|
player.src = "video/" + track_list[current_track].name
|
||||||
|
}
|
||||||
|
function checkStall(){
|
||||||
|
if (last_known_time == -1) {
|
||||||
|
last_known_time = player.currentTime
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (last_known_time == player.currentTime) {
|
||||||
|
nextVideo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.onload = function() {
|
||||||
|
player = document.getElementById("player")
|
||||||
|
player.autoplay = true
|
||||||
|
player.controls = true
|
||||||
|
load()
|
||||||
|
setInterval(videoEnd, 1000)
|
||||||
|
setInterval(load, 60000)
|
||||||
|
setInterval(checkStall, 30000)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript><h1 class="error">JavaScript is required for this page but not available</h1></noscript>
|
||||||
|
<div id="player-container" class="player-container">
|
||||||
|
<video id="player" class="player"></video>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{end}}
|
39
video.go
Normal file
39
video.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Video struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
DisplayName string `json:"display_name"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VideoIndex struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Generation int `json:"generation"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
Videos []Video `json:"videos"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func readVideoInfo(filepath string) (video Video, err error) {
|
||||||
|
if !strings.HasSuffix(filepath, ".json") {
|
||||||
|
filepath = filepath + ".json"
|
||||||
|
}
|
||||||
|
infoRaw, err := ioutil.ReadFile(filepath)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("can't read infofile \"%s\": %s", filepath, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(infoRaw, &video)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("can't read infofile \"%s\": %s", filepath, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user