Files
ffui/app.go
T

470 lines
12 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
2024-12-21 15:06:57 +02:00
var Checkmark = lipgloss.NewStyle().Foreground(lipgloss.Color("#22FF33")).Render("✔")
var X = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF2233")).Render("✖️")
2022-10-07 06:07:44 +03:00
const (
2024-12-20 23:25:13 +02:00
None Screen = iota
Files
Cfg
2022-10-07 06:07:44 +03:00
Main
)
2024-12-21 15:06:57 +02:00
type File struct {
Name string
Encode bool
}
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
2024-12-19 12:49:49 +02:00
Config []Config
VisibleConfig []Config
2022-10-07 06:07:44 +03:00
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"))
codecsStr, err := exec.Command("ffmpeg", "-encoders").Output()
2024-12-20 19:36:11 +02:00
if err != nil {
log.Println(err)
}
codecs := strings.Split(strings.Split(string(codecsStr), " ------\n")[1], "\n")
for i, codec := range codecs {
if codec == "" {
continue
}
codecs[i] = strings.Fields(codec)[1]
// hardcoded indexes are never bad YEP :)))
if contains(SupportedVideoEncoders, codecs[i]) {
Configs[2].Opts = append(Configs[2].Opts, codecs[i])
} else if contains(SupportedAudioEncoders, codecs[i]) {
Configs[3].Opts = append(Configs[3].Opts, codecs[i])
}
}
2024-12-20 19:36:11 +02:00
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,
2024-12-19 12:49:49 +02:00
Config: Configs,
VisibleConfig: getVisibleConfigs(Configs),
2022-10-08 02:36:15 +03:00
ErrQuit: false,
ErrQuitMessage: "",
2022-03-19 02:35:23 +02:00
}
}
2024-12-19 12:49:49 +02:00
func (m model) updateConfigFocusedOptions() {
for _, visCfg := range m.VisibleConfig {
for i := range m.Config {
if m.Config[i].Name == visCfg.Name {
m.Config[i].FocusedOption = visCfg.FocusedOption
}
}
}
}
2022-03-19 02:35:23 +02:00
func (m model) Init() tea.Cmd {
2024-12-20 23:25:13 +02:00
return tea.Batch(m.Spinner.Tick, m.statFiles)
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
2024-12-21 15:06:57 +02:00
return m, tea.Sequence(tea.ExitAltScreen, m.cleanUp)
2024-12-20 23:25:13 +02:00
case filesStatMsg:
m.FileCount = msg.fileCount
m.Files = msg.files
if len(m.Files) == 1 {
m.Screen = Cfg
} else if len(m.Files) > 1 {
m.Screen = Files
}
return m, nil
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 {
2024-12-20 23:25:13 +02:00
case Files:
// TODO: navigate files and select them and stuff
switch msg := msg.(type) {
case tea.KeyMsg:
key := msg.String()
switch key {
case "ctrl+c", "esc":
return m, tea.Quit
2024-12-21 15:06:57 +02:00
case "enter", "space":
// TODO: change the file state to Encode = true
// Also change the files to be of File type
// t := m.Files[m.FocusIndex]
case "g":
m.FocusIndex = 0
case "G":
m.FocusIndex = len(m.Files) - 1
case "tab", "shift+tab", "up", "down", "j", "k":
if key == "up" || key == "shift+tab" || key == "k" {
m.FocusIndex--
} else {
m.FocusIndex++
}
if m.FocusIndex >= len(m.Files) {
m.FocusIndex = 0
} else if m.FocusIndex < 0 {
m.FocusIndex = len(m.Files) - 1
}
2024-12-20 23:25:13 +02:00
}
}
2022-10-07 06:07:44 +03:00
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
2024-12-19 12:49:49 +02:00
if m.FocusIndex == len(m.VisibleConfig) {
return m, m.parseConfig(false)
}
// parse config, print it and exit
2024-12-19 12:49:49 +02:00
if m.FocusIndex == len(m.VisibleConfig)+1 {
return m, m.parseConfig(true)
2022-10-07 06:07:44 +03:00
}
2024-12-19 12:49:49 +02:00
case "g":
m.FocusIndex = 0
case "G":
m.FocusIndex = len(m.VisibleConfig) + 1
case "tab", "shift+tab", "up", "down", "j", "k":
if key == "up" || key == "shift+tab" || key == "k" {
2022-10-07 06:07:44 +03:00
m.FocusIndex--
} else {
m.FocusIndex++
}
2024-12-19 12:49:49 +02:00
if m.FocusIndex >= len(m.VisibleConfig)+2 {
2022-10-07 06:07:44 +03:00
m.FocusIndex = 0
} else if m.FocusIndex < 0 {
2024-12-19 12:49:49 +02:00
m.FocusIndex = len(m.VisibleConfig) + 1
2022-10-07 06:07:44 +03:00
}
2024-12-19 12:49:49 +02:00
case "left", "right", "h", "l":
// If we're not hovering a button
if m.FocusIndex < len(m.VisibleConfig) {
cfg := &m.VisibleConfig[m.FocusIndex]
if key == "right" || key == "l" {
2022-10-07 06:07:44 +03:00
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
}
2024-12-19 12:49:49 +02:00
m.updateConfigFocusedOptions()
m.VisibleConfig = getVisibleConfigs(m.Config)
2022-10-07 06:07:44 +03:00
}
}
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
2024-12-20 23:25:13 +02:00
return m, tea.Batch(m.Spinner.Tick, startEncoding)
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)
}
2024-12-21 15:06:57 +02:00
return m, tea.Sequence(tea.ExitAltScreen, tea.Quit)
}
2024-12-20 23:25:13 +02:00
case encodingStartedMsg:
if m.DryRun {
2024-12-21 15:06:57 +02:00
return m, tea.Sequence(tea.ExitAltScreen, 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 {
2024-12-21 15:06:57 +02:00
return m, tea.Sequence(tea.ExitAltScreen, 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
}
2024-12-19 12:49:49 +02:00
func parseConfig(cfg []Config) ParsedConfig {
vEncoder := find(cfg, "Video Encoder")
aEncoder := find(cfg, "Audio Encoder")
preset := find(cfg, "Preset")
crf := find(cfg, "Constant Rate Factor (CRF)")
return ParsedConfig{
DeleteOldVideo: find(cfg, "Delete old video(s)?").FocusedOption != 0,
SkipEncodedVid: find(cfg, "What should we do about encoded videos?").FocusedOption == 0,
VideoEncoder: vEncoder.Opts[vEncoder.FocusedOption],
AudioEncoder: aEncoder.Opts[aEncoder.FocusedOption],
Preset: preset.Opts[preset.FocusedOption],
CRF: crf.Opts[crf.FocusedOption],
}
}
2024-12-20 23:25:13 +02:00
func FilesScreenView(m model) string {
view := ""
// TODO: Create a nice view where we can select different videos and then select the encoding options and start
// encoding.
2024-12-21 15:06:57 +02:00
for i, file := range m.Files {
if m.FocusIndex == i {
view += fmt.Sprintf(FocusedConfig.Render("[%s] %s"), " ", filepath.Base(file))
} else {
view += fmt.Sprintf(BlurredConfig.Render("[%s] %s"), " ", filepath.Base(file))
}
2024-12-20 23:25:13 +02:00
}
2024-12-21 15:06:57 +02:00
view += "\n"
2024-12-20 23:25:13 +02:00
return view
}
2022-10-07 06:07:44 +03:00
func CfgScreenView(m model) string {
view := ""
2024-12-19 12:49:49 +02:00
for i, cfg := range m.VisibleConfig {
2022-10-07 06:07:44 +03:00
opts := ""
2022-10-07 06:07:44 +03:00
for j, opt := range cfg.Opts {
2024-12-19 12:49:49 +02:00
if m.VisibleConfig[i].FocusedOption == j {
2022-10-07 06:07:44 +03:00
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
}
2024-12-19 12:49:49 +02:00
if m.FocusIndex == len(m.VisibleConfig) {
view += FocusedStartButton
2022-10-08 02:36:15 +03:00
} else {
view += BlurredStartButton
}
2024-12-19 12:49:49 +02:00
if m.FocusIndex == len(m.VisibleConfig)+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')
}
2024-12-21 15:06:57 +02:00
return fmt.Sprintf("%s %s\n", Checkmark, sb.String())
} else {
2024-12-21 15:06:57 +02:00
return fmt.Sprintf("%s %d/%d files encoded\n", Checkmark, m.FileCount-len(m.Files), m.FileCount)
}
} else if m.Cancelled {
2024-12-21 15:06:57 +02:00
// TODO: should we clean up the file ourselves?
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 {
2024-12-21 15:06:57 +02:00
return fmt.Sprintf("%s %s\n", X, m.ErrQuitMessage)
2022-10-08 02:36:15 +03:00
}
2022-10-07 06:07:44 +03:00
switch m.Screen {
2024-12-20 23:25:13 +02:00
case Files:
return FilesScreenView(m)
2022-10-07 06:07:44 +03:00
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
}