Files
ffui/app.go
T

354 lines
8.7 KiB
Go
Raw Normal View History

2022-03-19 02:35:23 +02:00
package main
import (
"fmt"
"log"
2022-03-19 02:35:23 +02:00
"os"
"os/exec"
2022-03-19 02:35:23 +02:00
"path/filepath"
"strings"
2022-03-19 02:35:23 +02:00
"github.com/charmbracelet/bubbles/progress"
2022-03-19 02:35:23 +02:00
"github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
2022-10-07 06:07:44 +03:00
type Screen int
2022-03-20 15:51:34 +02:00
var checkmark = lipgloss.NewStyle().Foreground(lipgloss.Color("#22FF33")).Render("✔")
2022-10-08 02:36:15 +03:00
var x = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF2233")).Render("✖️")
2022-10-07 06:07:44 +03:00
const (
Cfg Screen = iota
Main
)
2022-03-19 02:35:23 +02:00
type model struct {
IsDirectory bool
Path string
CurrentFileName string
FileCount int
Files []string
Spinner spinner.Model
SingleFileProgressBar progress.Model
SingleFileProgress float64
TotalProgressBar progress.Model
TotalProgress float64
Estimate int
Program *tea.Program
Command *exec.Cmd
Quitting bool
Cancelled bool
2022-10-07 06:07:44 +03:00
Screen Screen
Config map[int]Config
FocusIndex int
2022-10-08 02:36:15 +03:00
ParsedConfig ParsedConfig
DryRun bool
2022-10-08 02:36:15 +03:00
ErrQuit bool
ErrQuitMessage string
2022-03-19 02:35:23 +02:00
}
func initialModel(fileInfo os.FileInfo, absolutePath string) *model {
s := spinner.New()
s.Spinner = spinner.Dot
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
2022-10-07 06:07:44 +03:00
cfg := make(map[int]Config)
for i := 0; i < len(Configs); i++ {
cfg[i] = Configs[i]
}
2022-03-19 02:35:23 +02:00
return &model{
IsDirectory: fileInfo.IsDir(),
CurrentFileName: fileInfo.Name(),
Path: absolutePath,
FileCount: 0,
Files: make([]string, 0),
Spinner: s,
SingleFileProgressBar: progress.New(progress.WithGradient("#1010ff", "#00ff00")),
SingleFileProgress: 0.0,
TotalProgressBar: progress.New(progress.WithDefaultGradient()),
TotalProgress: 0.0,
Quitting: false,
2022-10-07 06:07:44 +03:00
Config: cfg,
2022-10-08 02:36:15 +03:00
ErrQuit: false,
ErrQuitMessage: "",
2022-03-19 02:35:23 +02:00
}
}
func (m model) Init() tea.Cmd {
2022-10-07 06:07:44 +03:00
return tea.Batch(m.Spinner.Tick, initCfg)
2022-03-19 02:35:23 +02:00
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
switch msg := msg.(type) {
2022-10-08 02:36:15 +03:00
case errQuitMsg:
m.ErrQuitMessage = msg.msg
m.ErrQuit = true
return m, m.cleanUp
2022-10-07 06:07:44 +03:00
}
2022-03-19 02:35:23 +02:00
2022-10-07 06:07:44 +03:00
switch m.Screen {
case Cfg:
switch msg := msg.(type) {
case tea.KeyMsg:
key := msg.String()
switch key {
case "ctrl+c", "esc":
return m, tea.Quit
2022-10-07 06:07:44 +03:00
case "enter", " ":
2022-10-08 02:36:15 +03:00
// parse config and switch to main screen if we're focused on the start button
2022-10-07 06:07:44 +03:00
if m.FocusIndex == len(Configs) {
return m, m.parseConfig(false)
}
// parse config, print it and exit
if m.FocusIndex == len(Configs)+1 {
return m, m.parseConfig(true)
2022-10-07 06:07:44 +03:00
}
case "tab", "shift+tab", "up", "down":
if key == "up" || key == "shift+tab" {
m.FocusIndex--
} else {
m.FocusIndex++
}
if m.FocusIndex >= len(Configs)+2 {
2022-10-07 06:07:44 +03:00
m.FocusIndex = 0
} else if m.FocusIndex < 0 {
m.FocusIndex = len(Configs) + 1
2022-10-07 06:07:44 +03:00
}
case "left", "right":
if cfg, ok := m.Config[m.FocusIndex]; ok {
if key == "right" {
cfg.FocusedOption++
} else {
cfg.FocusedOption--
}
if cfg.FocusedOption >= len(cfg.Opts) {
cfg.FocusedOption = 0
} else if cfg.FocusedOption < 0 {
cfg.FocusedOption = len(cfg.Opts) - 1
}
m.Config[m.FocusIndex] = cfg
}
}
case quitMsg:
m.Quitting = true
return m, tea.Quit
2022-10-08 02:36:15 +03:00
case parsedCfgMsg:
m.Screen = Main
m.ParsedConfig = msg.parsedConfig
return m, tea.Batch(m.Spinner.Tick, m.statFile)
2022-10-07 06:07:44 +03:00
}
case Main:
switch msg := msg.(type) {
case tea.KeyMsg:
key := msg.String()
switch key {
case "ctrl+c":
m.Cancelled = true
err := m.Command.Process.Signal(os.Interrupt)
if err != nil {
log.Println("An error occurred when sending SIGINT to the ffmpeg process:")
log.Println(err)
}
return m, tea.Quit
}
2022-10-07 06:07:44 +03:00
case initUi:
m.FileCount = msg.fileCount
m.Files = msg.files
if m.DryRun {
return m, gracefullyQuit
}
2022-10-07 06:07:44 +03:00
return m, encodeVideo
case encodeVideoMsg:
m.CurrentFileName = filepath.Base(m.Files[len(m.Files)-1])
go func() {
fullFilePath := m.Files[len(m.Files)-1]
2022-10-08 02:36:15 +03:00
encode(fullFilePath, filepath.Base(fullFilePath), m.Program, m.ParsedConfig)
2022-10-07 06:07:44 +03:00
}()
return m, nil
case ffmpegProcessStart:
log.Printf("Running command: %s\n", msg.cmd.String())
m.Command = msg.cmd
2022-10-07 06:07:44 +03:00
return m, nil
case finishedEncodingVideo:
2022-10-08 02:36:15 +03:00
if m.ParsedConfig.DeleteOldVideo {
m.CurrentFileName = fmt.Sprintf("Deleting: %s", filepath.Base(m.Files[len(m.Files)-1]))
os.Remove(m.Files[len(m.Files)-1])
}
2022-10-07 06:07:44 +03:00
m.Files = m.Files[:len(m.Files)-1]
if len(m.Files) == 0 {
return m, gracefullyQuit
2022-10-07 06:07:44 +03:00
}
return m, encodeVideo
case updateProgress:
m.SingleFileProgress = msg.progress
m.TotalProgress = (1.0/float64(m.FileCount))*msg.progress + (1.0 / float64(m.FileCount) * float64(m.FileCount-len(m.Files)))
2022-10-07 06:07:44 +03:00
singleProgressCmd := m.SingleFileProgressBar.SetPercent(msg.progress)
totalProgressCmd := m.TotalProgressBar.SetPercent(m.TotalProgress)
return m, tea.Batch(singleProgressCmd, totalProgressCmd)
case updateEstimate:
m.Estimate = msg.estimate
2022-10-07 06:07:44 +03:00
case quitMsg:
m.Quitting = true
return m, tea.Quit
case progress.FrameMsg:
singleProgressModel, singleProgressCmd := m.SingleFileProgressBar.Update(msg)
m.SingleFileProgressBar = singleProgressModel.(progress.Model)
2022-03-19 02:35:23 +02:00
2022-10-07 06:07:44 +03:00
totalProgressModel, totalProgressCmd := m.TotalProgressBar.Update(msg)
m.TotalProgressBar = totalProgressModel.(progress.Model)
2022-03-19 02:35:23 +02:00
2022-10-07 06:07:44 +03:00
return m, tea.Batch(singleProgressCmd, totalProgressCmd)
2022-03-20 15:51:34 +02:00
}
2022-10-07 06:07:44 +03:00
m.Spinner, cmd = m.Spinner.Update(msg)
}
return m, cmd
}
func CfgScreenView(m model) string {
view := ""
2022-10-07 06:07:44 +03:00
for i, cfg := range Configs {
opts := ""
2022-10-07 06:07:44 +03:00
for j, opt := range cfg.Opts {
if m.Config[i].FocusedOption == j {
opts += FocusedOption.Render(opt)
} else {
opts += BlurredOption.Render(opt)
}
2022-10-07 06:07:44 +03:00
if j != len(cfg.Opts)-1 {
opts += ", "
}
}
2022-10-07 06:07:44 +03:00
if i == m.FocusIndex {
view += fmt.Sprintf("%s { %s }\n", FocusedConfig.Render(cfg.Name), opts)
} else {
view += fmt.Sprintf("%s { %s }\n", BlurredConfig.Render(cfg.Name), opts)
}
2022-03-19 02:35:23 +02:00
}
2022-10-08 02:36:15 +03:00
if m.FocusIndex == len(Configs) {
view += FocusedStartButton
2022-10-08 02:36:15 +03:00
} else {
view += BlurredStartButton
}
if m.FocusIndex == len(Configs)+1 {
view += FocusedDryRunButton
} else {
view += BlurredDryRunButton
2022-10-08 02:36:15 +03:00
}
view += "\n"
2022-03-19 02:35:23 +02:00
2022-10-07 06:07:44 +03:00
return view
2022-03-19 02:35:23 +02:00
}
2022-10-07 06:07:44 +03:00
func MainScreenView(m model) string {
progress := ""
if m.IsDirectory {
2022-10-07 02:45:32 +03:00
progress = fmt.Sprintf("%s %d/%d files encoded\nFile Progress: %s\n\nTotal Progress: %s",
m.Spinner.View(),
m.FileCount-len(m.Files),
m.FileCount,
m.SingleFileProgressBar.View(),
m.TotalProgressBar.View())
} else {
2022-10-07 02:45:32 +03:00
progress = fmt.Sprintf("%s %d/%d files encoded\n%s",
m.Spinner.View(),
m.FileCount-len(m.Files),
m.FileCount,
m.SingleFileProgressBar.View())
}
eta := formatEstimate(m.Estimate)
view := fmt.Sprintf("\nEncoding \"%s\"... ETA: %s\n%s", m.CurrentFileName, eta, progress)
return view
2022-03-19 02:35:23 +02:00
}
func formatEstimate(estimate int) string {
if estimate >= 86400 {
day := estimate / 86400
hour := (estimate % 86400) / 3600
min := (estimate % 3600) / 60
sec := estimate % 60
return fmt.Sprintf("%dd%dh%dm%ds", day, hour, min, sec)
} else if estimate >= 3600 {
hour := estimate / 3600
min := (estimate % 3600) / 60
sec := estimate % 60
return fmt.Sprintf("%dh%dm%ds", hour, min, sec)
} else if estimate >= 60 {
min := estimate / 60
sec := estimate % 60
return fmt.Sprintf("%dm%ds", min, sec)
}
return fmt.Sprintf("%ds", estimate)
}
2022-10-07 06:07:44 +03:00
func (m model) View() string {
if m.Quitting {
if m.DryRun {
sb := strings.Builder{}
for _, file := range m.Files {
parentDir := filepath.Dir(file)
fileName := filepath.Base(file)
extensionIndex := strings.LastIndex(fileName, ".")
newFileName := fileName[:extensionIndex]
extension := fileName[extensionIndex:]
outFileFullPath := filepath.Join(parentDir, newFileName+fmt.Sprintf("_[%s]_[%s]", m.ParsedConfig.VideoEncoder, m.ParsedConfig.AudioEncoder)+extension)
cmd := exec.Command("ffmpeg", buildFFmpegCmdArgs(file, outFileFullPath, m.ParsedConfig)...)
sb.WriteString(cmd.String())
sb.WriteByte('\n')
}
return fmt.Sprintf("%s %s\n", checkmark, sb.String())
} else {
return fmt.Sprintf("%s %d/%d files encoded\n", checkmark, m.FileCount-len(m.Files), m.FileCount)
}
} else if m.Cancelled {
return fmt.Sprintf("%s Encoding cancelled. Stopped ffmpeg process.\n Make sure to clean up the created file\n", x)
2022-03-19 02:35:23 +02:00
}
2022-10-08 02:36:15 +03:00
if m.ErrQuit {
return fmt.Sprintf("%s %s\n", x, m.ErrQuitMessage)
}
2022-10-07 06:07:44 +03:00
switch m.Screen {
case Cfg:
return CfgScreenView(m)
case Main:
return MainScreenView(m)
2022-03-19 02:35:23 +02:00
}
2022-03-20 15:51:34 +02:00
2022-10-07 06:07:44 +03:00
return "Error: Invalid Screen"
2022-03-20 15:51:34 +02:00
}