TUISTREAM/cmd/tuistream/main.go
28allday 77929de645 Spin down idle media drives by default via a watcher service
Drives on a headless Jellyfin box ran hot 24/7: NAS-class drives (WD
Red, IronWolf) ship with no idle timer at all. v0.1.2 stopped tuistream
from keeping them awake; this makes them actually go to sleep.

Spin-down is a DEFAULT, not a feature: when the inventory shows
spinning media drives, the TUI silently installs/syncs the mechanism
and flashes what it did. [s] on Setup is the opt-OUT, remembered via
/etc/tuistream/spindown-off so the default never fights the user.

The mechanism is a tiny systemd service (tuistream --spindown-watch)
that samples /proc/diskstats and issues `hdparm -y` to any target drive
idle past 3 minutes. We deliberately do NOT use the drive's own standby
timer (hdparm -S): the 10TB helium WD Reds advertise it and then ignore
it — verified on real hardware (fresh -S 36, zero I/O, four minutes,
still active/idle). Forcing standby from outside works on everything.
An rc shipped the udev+hdparm -S approach; the plans clean its rule up.

Safety: targets are spinning (sysfs rotational), top-level, real disks,
never a system disk — same classifier the rest of Setup trusts. The
watcher's own probes can't disturb drives: /proc/diskstats and sysfs are
kernel memory, and hdparm -C (CHECK POWER MODE) doesn't wake or reset
anything. The unit's ExecStart points at the running binary and the
auto-sync rewrites it when the content goes stale (e.g. binary moved
from a dev path to /usr/local/bin).

hdparm joins the dependency pre-flight. Verified end-to-end on moviebox
(2x WD Red 10TB SATA + IronWolf 10TB USB): auto-applied on launch, all
three drives reached standby after 3 idle minutes, Monitor shows them
as "asleep" without waking them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 16:28:39 +01:00

86 lines
2.6 KiB
Go

// Command tuistream is a TUI for setting up and managing a headless Jellyfin
// Media Server on Omarchy / Arch Linux.
//
// Two tabs:
//
// Setup — install Jellyfin, attach media drives, firewall, uninstall.
// Manage — copy files from an external drive to a media drive,
// delete files, rename files.
//
// The picker hides any partition that lives on a disk hosting the OS or
// active swap, so the boot drive can never be selected as a media drive.
package main
import (
"flag"
"fmt"
"os"
tea "github.com/charmbracelet/bubbletea"
"tuistream/internal/spindown"
"tuistream/internal/system"
"tuistream/internal/theme"
"tuistream/internal/tui"
)
// version is stamped at build time via:
//
// -ldflags "-X main.version=v0.1.1"
var version = "dev"
func main() {
readOnly := flag.Bool("read-only", false,
"open the TUI without checking for root; only the inventory views work")
showVersion := flag.Bool("version", false, "print the version and exit")
spindownWatch := flag.Bool("spindown-watch", false,
"internal: run the drive idle watcher (started by tuistream-spindown.service)")
flag.Parse()
if *showVersion {
fmt.Println("tuistream", version)
return
}
// Daemon mode: no TUI, just the idle watcher (root needed for hdparm).
if *spindownWatch {
if os.Geteuid() != 0 {
fmt.Fprintln(os.Stderr, "tuistream --spindown-watch must run as root")
os.Exit(1)
}
if err := spindown.Watch(); err != nil {
fmt.Fprintln(os.Stderr, "tuistream:", err)
os.Exit(1)
}
return
}
if !*readOnly && os.Geteuid() != 0 {
fmt.Fprintln(os.Stderr,
"tuistream needs administrator rights to install Jellyfin, edit /etc/fstab, etc.")
fmt.Fprintln(os.Stderr, " Run: sudo tuistream")
fmt.Fprintln(os.Stderr, " Or: tuistream --read-only (inventory only, no actions)")
os.Exit(1)
}
// Pre-flight: install any userspace tools the flows assume are present.
// Only attempted when running as root — read-only just skips the check.
if !*readOnly {
if err := system.EnsureInstalled(); err != nil {
fmt.Fprintln(os.Stderr, "tuistream: pacman failed:", err)
fmt.Fprintln(os.Stderr,
" Some actions may not work until the missing packages are installed.")
}
}
m := tui.NewModel(theme.Load())
// No mouse capture: TUISTREAM is keyboard-driven, and grabbing the mouse
// would stop the user's terminal/tmux from selecting-to-copy text (e.g.
// the Jellyfin web URL) or click-opening links. Leaving it off keeps
// native selection and hyperlink handling working over SSH + tmux.
p := tea.NewProgram(m, tea.WithAltScreen())
if _, err := p.Run(); err != nil {
fmt.Fprintln(os.Stderr, "tuistream:", err)
os.Exit(1)
}
}