cmd/tvwall/main.go
130 Zeilen · 3.4 KB · Go
| 1 | // Kommando tvwall: Media-Player und Webinterface fuer die TV-Wall. |
| 2 | // |
| 3 | // Der Dienst liest regelmaessig den Medienordner ein, haelt daraus eine |
| 4 | // Playliste aktuell, laesst sie von mpv im Vollbild in Endlosschleife |
| 5 | // abspielen und stellt zum Hochladen und Sortieren ein Webinterface bereit. |
| 6 | package main |
| 7 | |
| 8 | import ( |
| 9 | "context" |
| 10 | "errors" |
| 11 | "flag" |
| 12 | "fmt" |
| 13 | "log" |
| 14 | "net/http" |
| 15 | "os" |
| 16 | "os/signal" |
| 17 | "syscall" |
| 18 | "time" |
| 19 | |
| 20 | "ehc/tvwall/internal/config" |
| 21 | "ehc/tvwall/internal/player" |
| 22 | "ehc/tvwall/internal/playlist" |
| 23 | "ehc/tvwall/internal/web" |
| 24 | ) |
| 25 | |
| 26 | // version wird beim Bauen ueber -ldflags gesetzt. |
| 27 | var version = "dev" |
| 28 | |
| 29 | func main() { |
| 30 | var ( |
| 31 | configPath = flag.String("config", "/etc/tvwall/tvwall.conf", "Pfad zur Konfigurationsdatei") |
| 32 | addr = flag.String("addr", "", "Adresse des Webservers (ueberschreibt WEB_PORT)") |
| 33 | stateDir = flag.String("state-dir", "", "Datenverzeichnis (ueberschreibt STATE_DIR)") |
| 34 | mpvSocket = flag.String("mpv-socket", "", "Pfad des mpv-IPC-Sockets (ueberschreibt MPV_SOCKET)") |
| 35 | noPlayer = flag.Bool("no-player", false, "nur das Webinterface starten, kein mpv") |
| 36 | showVer = flag.Bool("version", false, "Version ausgeben und beenden") |
| 37 | ) |
| 38 | flag.Parse() |
| 39 | |
| 40 | if *showVer { |
| 41 | fmt.Printf("tvwall %s\n", version) |
| 42 | return |
| 43 | } |
| 44 | |
| 45 | log.SetFlags(log.Ldate | log.Ltime) |
| 46 | log.Printf("tvwall %s startet", version) |
| 47 | |
| 48 | cfg, err := config.Load(*configPath) |
| 49 | if err != nil { |
| 50 | log.Fatalf("konfiguration %s: %v", *configPath, err) |
| 51 | } |
| 52 | if *stateDir != "" { |
| 53 | cfg.StateDir = *stateDir |
| 54 | } |
| 55 | if *mpvSocket != "" { |
| 56 | cfg.SocketPath = *mpvSocket |
| 57 | } |
| 58 | for _, dir := range []string{cfg.StateDir, cfg.MediaDir(), cfg.ThumbDir()} { |
| 59 | if err := os.MkdirAll(dir, 0o775); err != nil { |
| 60 | log.Fatalf("verzeichnis %s anlegen: %v", dir, err) |
| 61 | } |
| 62 | } |
| 63 | log.Printf("medienordner: %s", cfg.MediaDir()) |
| 64 | |
| 65 | settings := config.NewStore(cfg) |
| 66 | pl := playlist.New(cfg) |
| 67 | if _, err := pl.Scan(); err != nil { |
| 68 | log.Printf("erster durchlauf des medienordners: %v", err) |
| 69 | } |
| 70 | log.Printf("playliste: %d eintraege", len(pl.Entries())) |
| 71 | |
| 72 | ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) |
| 73 | defer stop() |
| 74 | |
| 75 | pp := player.New(cfg, pl, settings) |
| 76 | if *noPlayer { |
| 77 | log.Printf("player deaktiviert (-no-player)") |
| 78 | } else { |
| 79 | go pp.Run(ctx) |
| 80 | } |
| 81 | |
| 82 | go watchMedia(ctx, pl, time.Duration(cfg.ScanInterval)*time.Second) |
| 83 | |
| 84 | listen := *addr |
| 85 | if listen == "" { |
| 86 | listen = fmt.Sprintf(":%d", cfg.WebPort) |
| 87 | } |
| 88 | srv := web.New(cfg, pl, pp, settings) |
| 89 | |
| 90 | errCh := make(chan error, 1) |
| 91 | go func() { |
| 92 | if err := srv.ListenAndServe(listen); err != nil && !errors.Is(err, http.ErrServerClosed) { |
| 93 | errCh <- err |
| 94 | } |
| 95 | }() |
| 96 | |
| 97 | select { |
| 98 | case <-ctx.Done(): |
| 99 | log.Printf("beende …") |
| 100 | // Dem Player einen Moment geben, mpv sauber zu beenden. |
| 101 | time.Sleep(300 * time.Millisecond) |
| 102 | case err := <-errCh: |
| 103 | log.Fatalf("webserver: %v", err) |
| 104 | } |
| 105 | } |
| 106 | |
| 107 | // watchMedia liest den Medienordner regelmaessig ein. So landen auch Dateien |
| 108 | // in der Playliste, die per USB-Stick, SSH oder Netzwerkfreigabe ankommen. |
| 109 | func watchMedia(ctx context.Context, pl *playlist.Manager, every time.Duration) { |
| 110 | if every < time.Second { |
| 111 | every = 10 * time.Second |
| 112 | } |
| 113 | ticker := time.NewTicker(every) |
| 114 | defer ticker.Stop() |
| 115 | for { |
| 116 | select { |
| 117 | case <-ctx.Done(): |
| 118 | return |
| 119 | case <-ticker.C: |
| 120 | changed, err := pl.Scan() |
| 121 | if err != nil { |
| 122 | log.Printf("medienordner einlesen: %v", err) |
| 123 | continue |
| 124 | } |
| 125 | if changed { |
| 126 | log.Printf("medienordner geaendert: %d eintraege", len(pl.Entries())) |
| 127 | } |
| 128 | } |
| 129 | } |
| 130 | } |