Git Repository

Mini Video Library

Mini Video Library scannt lokale Medienordner, verwaltet Metadaten und Vorschaubilder und streamt kompatible Videos direkt per HTTP an den Browser.

Projektseite ↗
HTTPShttps://zanvex.de/git/mini-video-library.git

cmd/video-library/main.go

Zum Verzeichnis
package main

import (
	"context"
	"flag"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	"minivideolib/internal/config"
	"minivideolib/internal/database"
	"minivideolib/internal/library"
	"minivideolib/internal/logging"
	"minivideolib/internal/plugins"
	"minivideolib/internal/scanner"
	"minivideolib/internal/thumbnail"
	webapp "minivideolib/internal/web"
)

func main() {
	configPath := flag.String("config", "config.toml", "path to the TOML configuration")
	scanOnly := flag.Bool("scan", false, "scan once and exit")
	flag.Parse()

	logStore := logging.NewStore(1000)
	logOutput := io.Writer(os.Stdout)
	if fileLog, e := logging.NewRotatingWriter("data/logs/video-library.log", 10<<20); e == nil {
		defer fileLog.Close()
		logOutput = io.MultiWriter(os.Stdout, fileLog)
	}
	logger := slog.New(logging.NewHandler(slog.NewTextHandler(logOutput, &slog.HandlerOptions{Level: slog.LevelInfo}), logStore))
	cfg, err := config.Load(*configPath)
	if err != nil {
		logger.Error("load config", "error", err)
		os.Exit(1)
	}
	db, err := database.Open(cfg.DatabasePath)
	if err != nil {
		logger.Error("open database", "error", err)
		os.Exit(1)
	}
	defer db.Close()

	thumbs := thumbnail.New(cfg.FFmpegPath, cfg.ThumbnailDir, cfg.ThumbnailWidth, cfg.ThumbnailCount, logger)
	scan := scanner.New(db, cfg, thumbs, logger)
	ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer cancel()
	if *scanOnly {
		if err := scan.Run(ctx); err != nil {
			logger.Error("scan", "error", err)
			os.Exit(1)
		}
		return
	}
	go func() {
		if cfg.ScanInterval > 0 {
			t := time.NewTicker(30 * time.Second)
			defer t.Stop()
			last := time.Now()
			for {
				select {
				case <-ctx.Done():
					return
				case <-t.C:
					interval := scan.Config().ScanInterval
					if interval <= 0 || time.Since(last) < interval {
						continue
					}
					if err := scan.Run(ctx); err != nil {
						logger.Error("scheduled scan", "error", err)
					}
					last = time.Now()
				}
			}
		}
	}()

	lib := library.New(db)
	pluginService := plugins.New(db)
	handler, err := webapp.New(lib, db, scan, cfg, *configPath, logger, logStore, pluginService)
	if err != nil {
		logger.Error("initialize web", "error", err)
		os.Exit(1)
	}
	srv := &http.Server{Addr: cfg.ListenAddress, Handler: handler, ReadHeaderTimeout: 10 * time.Second, IdleTimeout: 2 * time.Minute}
	go func() {
		logger.Info("video library ready", "url", "http://"+cfg.ListenAddress)
		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			logger.Error("http server", "error", err)
			cancel()
		}
	}()
	<-ctx.Done()
	shutdown, stop := context.WithTimeout(context.Background(), 10*time.Second)
	defer stop()
	if err := srv.Shutdown(shutdown); err != nil {
		fmt.Fprintln(os.Stderr, err)
	}
}