Compare commits

...
5 Commits
Author SHA1 Message Date
Illusion 2951ea391f todo 2024-12-27 02:26:20 +02:00
Illusion 95bedb8ee7 feat: allow selection for files to be encoded in a directory 2024-12-27 00:07:05 +02:00
Illusion 85ebb57885 put buttons next to each other 2024-12-26 21:12:20 +02:00
Illusion 1d456b9863 clearer(?) config name for when output file name conflicts with another 2024-12-26 20:52:12 +02:00
Illusion 2e1d175e98 misc: small clean ups and fixes
fix: dry run command truncation by printing the command after bubbletea quits
Remove unnecessary startEncoding tea.Cmd
Capitalize model struct to Model
2024-12-26 18:34:33 +02:00
8 changed files with 321 additions and 157 deletions
+3 -2
View File
@@ -1,5 +1,6 @@
- [x] query ffmpeg binary for available encoders and list the ones we know - [x] query ffmpeg binary for available encoders and list the ones we know
- [ ] when passing in a directory, list all video files in that directory and have the user choose which ones they want encoded - [-] when passing in a directory, list all video files in that directory and have the user choose which ones they want encoded
- This will of course have a select all checkbox for quickly encoding all the files in a dir. - [ ] Scrollable viewport for the files. They currently get cut if there's too many.
- [ ] CLI Option/Flag to choose the output directory for the encoded video(s)
- [ ] calculate total progress based on file sizes instead of counts of files for a more accurate progress percentage. - [ ] calculate total progress based on file sizes instead of counts of files for a more accurate progress percentage.
might not do this. might not do this.
+162 -81
View File
@@ -21,22 +21,22 @@ var X = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF2233")).Render("✖️
const ( const (
None Screen = iota None Screen = iota
Files
Cfg Cfg
Files
Main Main
) )
type File struct { type File struct {
Name string Path string
Encode bool Selected bool
} }
type model struct { type Model struct {
IsDirectory bool IsDirectory bool
Path string Path string
CurrentFileName string CurrentFileName string
FileCount int FileCount int
Files []string Files []File
Spinner spinner.Model Spinner spinner.Model
SingleFileProgressBar progress.Model SingleFileProgressBar progress.Model
SingleFileProgress float64 SingleFileProgress float64
@@ -51,13 +51,16 @@ type model struct {
Config []Config Config []Config
VisibleConfig []Config VisibleConfig []Config
FocusIndex int FocusIndex int
ChoiceIndex int
ParsedConfig ParsedConfig ParsedConfig ParsedConfig
DryRun bool DryRun bool
ErrQuit bool ErrQuit bool
ErrQuitMessage string ErrQuitMessage string
} }
func initialModel(fileInfo os.FileInfo, absolutePath string) *model { // We're returning a pointer here so we can embed the tea.Program on the original model
// instead of a copy.
func initialModel(fileInfo os.FileInfo, absolutePath string) *Model {
s := spinner.New() s := spinner.New()
s.Spinner = spinner.Dot s.Spinner = spinner.Dot
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
@@ -83,12 +86,13 @@ func initialModel(fileInfo os.FileInfo, absolutePath string) *model {
} }
} }
return &model{ return &Model{
IsDirectory: fileInfo.IsDir(), IsDirectory: fileInfo.IsDir(),
CurrentFileName: fileInfo.Name(), CurrentFileName: fileInfo.Name(),
Path: absolutePath, Path: absolutePath,
FileCount: 0, FileCount: 0,
Files: make([]string, 0), Files: make([]File, 0),
Screen: Cfg,
Spinner: s, Spinner: s,
SingleFileProgressBar: progress.New(progress.WithGradient("#1010ff", "#00ff00")), SingleFileProgressBar: progress.New(progress.WithGradient("#1010ff", "#00ff00")),
SingleFileProgress: 0.0, SingleFileProgress: 0.0,
@@ -102,7 +106,7 @@ func initialModel(fileInfo os.FileInfo, absolutePath string) *model {
} }
} }
func (m model) updateConfigFocusedOptions() { func (m Model) updateConfigFocusedOptions() {
for _, visCfg := range m.VisibleConfig { for _, visCfg := range m.VisibleConfig {
for i := range m.Config { for i := range m.Config {
if m.Config[i].Name == visCfg.Name { if m.Config[i].Name == visCfg.Name {
@@ -112,11 +116,11 @@ func (m model) updateConfigFocusedOptions() {
} }
} }
func (m model) Init() tea.Cmd { func (m Model) Init() tea.Cmd {
return tea.Batch(m.Spinner.Tick, m.statFiles) return tea.Batch(m.Spinner.Tick, m.statFiles)
} }
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd var cmd tea.Cmd
switch msg := msg.(type) { switch msg := msg.(type) {
@@ -128,32 +132,61 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.FileCount = msg.fileCount m.FileCount = msg.fileCount
m.Files = msg.files m.Files = msg.files
if len(m.Files) == 1 {
m.Screen = Cfg
} else if len(m.Files) > 1 {
m.Screen = Files
}
return m, nil return m, nil
} }
switch m.Screen { switch m.Screen {
case Files: case Files:
// TODO: navigate files and select them and stuff
switch msg := msg.(type) { switch msg := msg.(type) {
case tea.KeyMsg: case tea.KeyMsg:
key := msg.String() key := msg.String()
switch key { switch key {
case "ctrl+c", "esc": case "ctrl+c", "esc":
return m, tea.Quit return m, tea.Quit
case "enter", "space": case "enter", " ":
// TODO: change the file state to Encode = true if m.FocusIndex == 0 && key == "enter" {
// Also change the files to be of File type selectAll := !every(m.Files, func(e File) bool { return e.Selected })
// t := m.Files[m.FocusIndex]
if m.ChoiceIndex == 0 {
for i := range m.Files {
m.Files[i].Selected = selectAll
}
} else {
m.Files = filter(m.Files, func(f File) bool {
return f.Selected
})
m.Screen = Main
m.FileCount = len(m.Files)
return m, tea.Batch(m.Spinner.Tick, encodeVideo)
}
} else if m.FocusIndex != 0 {
m.Files[m.FocusIndex-1].Selected = !m.Files[m.FocusIndex-1].Selected
// Reset the choice to "Select All" button when no files are selected
if !anyOf(m.Files, func(f File) bool { return f.Selected }) {
m.ChoiceIndex = 0
}
}
case "g": case "g":
m.FocusIndex = 0 m.FocusIndex = 0
case "G": case "G":
m.FocusIndex = len(m.Files) - 1 m.FocusIndex = len(m.Files)
case "left", "right", "h", "l":
if m.FocusIndex == 0 && anyOf(m.Files, func(f File) bool { return f.Selected }) {
if key == "right" || key == "l" {
m.ChoiceIndex++
} else {
m.ChoiceIndex--
}
if m.ChoiceIndex > 1 {
m.ChoiceIndex = 0
} else if m.ChoiceIndex < 0 {
m.ChoiceIndex = 1
}
}
case "tab", "shift+tab", "up", "down", "j", "k": case "tab", "shift+tab", "up", "down", "j", "k":
if key == "up" || key == "shift+tab" || key == "k" { if key == "up" || key == "shift+tab" || key == "k" {
m.FocusIndex-- m.FocusIndex--
@@ -161,10 +194,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.FocusIndex++ m.FocusIndex++
} }
if m.FocusIndex >= len(m.Files) { if m.FocusIndex >= len(m.Files)+1 {
m.FocusIndex = 0 m.FocusIndex = 0
} else if m.FocusIndex < 0 { } else if m.FocusIndex < 0 {
m.FocusIndex = len(m.Files) - 1 m.FocusIndex = len(m.Files)
} }
} }
} }
@@ -178,16 +211,16 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case "enter", " ": case "enter", " ":
// parse config and switch to main screen if we're focused on the start button // parse config and switch to main screen if we're focused on the start button
if m.FocusIndex == len(m.VisibleConfig) { if m.FocusIndex == len(m.VisibleConfig) {
if m.ChoiceIndex == 0 {
return m, m.parseConfig(false) return m, m.parseConfig(false)
} } else if m.ChoiceIndex == 1 {
// parse config, print it and exit
if m.FocusIndex == len(m.VisibleConfig)+1 {
return m, m.parseConfig(true) return m, m.parseConfig(true)
} }
}
case "g": case "g":
m.FocusIndex = 0 m.FocusIndex = 0
case "G": case "G":
m.FocusIndex = len(m.VisibleConfig) + 1 m.FocusIndex = len(m.VisibleConfig)
case "tab", "shift+tab", "up", "down", "j", "k": case "tab", "shift+tab", "up", "down", "j", "k":
if key == "up" || key == "shift+tab" || key == "k" { if key == "up" || key == "shift+tab" || key == "k" {
m.FocusIndex-- m.FocusIndex--
@@ -195,10 +228,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.FocusIndex++ m.FocusIndex++
} }
if m.FocusIndex >= len(m.VisibleConfig)+2 { if m.FocusIndex > len(m.VisibleConfig) {
m.FocusIndex = 0 m.FocusIndex = 0
} else if m.FocusIndex < 0 { } else if m.FocusIndex < 0 {
m.FocusIndex = len(m.VisibleConfig) + 1 m.FocusIndex = len(m.VisibleConfig)
} }
case "left", "right", "h", "l": case "left", "right", "h", "l":
// If we're not hovering a button // If we're not hovering a button
@@ -218,15 +251,39 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.updateConfigFocusedOptions() m.updateConfigFocusedOptions()
m.VisibleConfig = getVisibleConfigs(m.Config) m.VisibleConfig = getVisibleConfigs(m.Config)
} else {
if key == "right" || key == "l" {
m.ChoiceIndex++
} else {
m.ChoiceIndex--
}
if m.ChoiceIndex > 1 {
m.ChoiceIndex = 0
} else if m.ChoiceIndex < 0 {
m.ChoiceIndex = 1
}
} }
} }
case quitMsg: case quitMsg:
m.Quitting = true m.Quitting = true
return m, tea.Quit return m, tea.Quit
case parsedCfgMsg: case parsedCfgMsg:
m.Screen = Main
m.ParsedConfig = msg.parsedConfig m.ParsedConfig = msg.parsedConfig
return m, tea.Batch(m.Spinner.Tick, startEncoding)
if m.DryRun {
return m, tea.Batch(tea.ExitAltScreen, tea.Quit)
}
if m.IsDirectory {
m.Screen = Files
m.FocusIndex = 0
m.ChoiceIndex = 0
} else {
m.Screen = Main
return m, tea.Batch(m.Spinner.Tick, encodeVideo)
}
} }
case Main: case Main:
switch msg := msg.(type) { switch msg := msg.(type) {
@@ -242,18 +299,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
return m, tea.Sequence(tea.ExitAltScreen, tea.Quit) return m, tea.Sequence(tea.ExitAltScreen, tea.Quit)
} }
case encodingStartedMsg:
if m.DryRun {
return m, tea.Sequence(tea.ExitAltScreen, gracefullyQuit)
}
return m, encodeVideo
case encodeVideoMsg: case encodeVideoMsg:
m.CurrentFileName = filepath.Base(m.Files[len(m.Files)-1]) m.CurrentFileName = filepath.Base(m.Files[len(m.Files)-1].Path)
go func() { go func() {
fullFilePath := m.Files[len(m.Files)-1] file := m.Files[len(m.Files)-1]
encode(fullFilePath, filepath.Base(fullFilePath), m.Program, m.ParsedConfig) encode(file, filepath.Base(file.Path), m.Program, m.ParsedConfig)
}() }()
return m, nil return m, nil
@@ -263,8 +314,8 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil return m, nil
case finishedEncodingVideo: case finishedEncodingVideo:
if m.ParsedConfig.DeleteOldVideo { if m.ParsedConfig.DeleteOldVideo {
m.CurrentFileName = fmt.Sprintf("Deleting: %s", filepath.Base(m.Files[len(m.Files)-1])) m.CurrentFileName = fmt.Sprintf("Deleting: %s", filepath.Base(m.Files[len(m.Files)-1].Path))
os.Remove(m.Files[len(m.Files)-1]) os.Remove(m.Files[len(m.Files)-1].Path)
} }
m.Files = m.Files[:len(m.Files)-1] m.Files = m.Files[:len(m.Files)-1]
@@ -311,7 +362,7 @@ func parseConfig(cfg []Config) ParsedConfig {
return ParsedConfig{ return ParsedConfig{
DeleteOldVideo: find(cfg, "Delete old video(s)?").FocusedOption != 0, DeleteOldVideo: find(cfg, "Delete old video(s)?").FocusedOption != 0,
SkipEncodedVid: find(cfg, "What should we do about encoded videos?").FocusedOption == 0, IgnoreConflictingName: find(cfg, "On name conflict?").FocusedOption == 0,
VideoEncoder: vEncoder.Opts[vEncoder.FocusedOption], VideoEncoder: vEncoder.Opts[vEncoder.FocusedOption],
AudioEncoder: aEncoder.Opts[aEncoder.FocusedOption], AudioEncoder: aEncoder.Opts[aEncoder.FocusedOption],
Preset: preset.Opts[preset.FocusedOption], Preset: preset.Opts[preset.FocusedOption],
@@ -319,25 +370,58 @@ func parseConfig(cfg []Config) ParsedConfig {
} }
} }
func FilesScreenView(m model) string { func FilesScreenView(m Model) string {
view := "" view := lipgloss.NewStyle().Margin(1, 0).Render("Select the files you wish to encode.")
var files string
var buttons string
var selectAllBtnText string
// TODO: Create a nice view where we can select different videos and then select the encoding options and start allSelected := every(m.Files, func(e File) bool { return e.Selected })
// encoding. noneSelected := !anyOf(m.Files, func(e File) bool { return e.Selected })
for i, file := range m.Files {
if m.FocusIndex == i { if allSelected {
view += fmt.Sprintf(FocusedConfig.Render("[%s] %s"), " ", filepath.Base(file)) selectAllBtnText = "Deselect All"
} else { } else {
view += fmt.Sprintf(BlurredConfig.Render("[%s] %s"), " ", filepath.Base(file)) selectAllBtnText = "Select All"
}
blurredStartButton := BlurredStartButton
focusedStartButton := FocusedStartButton
if noneSelected {
blurredStartButton = DisabledStartButton
focusedStartButton = DisabledStartButton
}
if m.FocusIndex == 0 {
if m.ChoiceIndex == 0 {
buttons = lipgloss.JoinHorizontal(0, FocusedSelectAllButton.Render(selectAllBtnText), blurredStartButton)
} else {
buttons = lipgloss.JoinHorizontal(0, BlurredSelectAllButton.Render(selectAllBtnText), focusedStartButton)
}
} else {
buttons = lipgloss.JoinHorizontal(0, BlurredSelectAllButton.Render(selectAllBtnText), blurredStartButton)
}
// TODO: files should be a scrollable grid
for i, file := range m.Files {
selection := " "
if file.Selected {
selection = "x"
}
if m.FocusIndex == i+1 {
files += fmt.Sprintf(FocusedConfig.Render("[%s] %s"), selection, filepath.Base(file.Path))
} else {
files += fmt.Sprintf(BlurredConfig.Render("[%s] %s"), selection, filepath.Base(file.Path))
} }
} }
view += "\n" view += lipgloss.JoinVertical(0, buttons, files)
return view return view
} }
func CfgScreenView(m model) string { func CfgScreenView(m Model) string {
view := "" view := ""
for i, cfg := range m.VisibleConfig { for i, cfg := range m.VisibleConfig {
@@ -362,27 +446,39 @@ func CfgScreenView(m model) string {
} }
} }
if m.FocusIndex == len(m.VisibleConfig) { var startButton string
view += FocusedStartButton var dryRunButton string
if m.FocusIndex == len(m.VisibleConfig) && m.ChoiceIndex == 0 {
if m.IsDirectory {
startButton = FocusedNextButton
} else { } else {
view += BlurredStartButton startButton = FocusedStartButton
}
} else {
if m.IsDirectory {
startButton = BlurredNextButton
} else {
startButton = BlurredStartButton
}
} }
if m.FocusIndex == len(m.VisibleConfig)+1 { if m.FocusIndex == len(m.VisibleConfig) && m.ChoiceIndex == 1 {
view += FocusedDryRunButton dryRunButton = FocusedDryRunButton
} else { } else {
view += BlurredDryRunButton dryRunButton = BlurredDryRunButton
} }
view += lipgloss.JoinHorizontal(0, startButton, dryRunButton)
view += "\n" view += "\n"
return view return view
} }
func MainScreenView(m model) string { func MainScreenView(m Model) string {
progress := "" progress := ""
if m.IsDirectory { if m.IsDirectory && m.FileCount > 1 {
progress = fmt.Sprintf("%s %d/%d files encoded\nFile Progress: %s\n\nTotal Progress: %s", progress = fmt.Sprintf("%s %d/%d files encoded\nFile Progress: %s\n\nTotal Progress: %s",
m.Spinner.View(), m.Spinner.View(),
m.FileCount-len(m.Files), m.FileCount-len(m.Files),
@@ -425,28 +521,13 @@ func formatEstimate(estimate int) string {
return fmt.Sprintf("%ds", estimate) return fmt.Sprintf("%ds", estimate)
} }
func (m model) View() string { func (m Model) View() string {
if m.Quitting {
if m.DryRun { if m.DryRun {
sb := strings.Builder{} return ""
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()) if m.Quitting {
} else {
return fmt.Sprintf("%s %d/%d files encoded\n", Checkmark, m.FileCount-len(m.Files), m.FileCount) return fmt.Sprintf("%s %d/%d files encoded\n", Checkmark, m.FileCount-len(m.Files), m.FileCount)
}
} else if m.Cancelled { } else if m.Cancelled {
// TODO: should we clean up the file ourselves? // 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) return fmt.Sprintf("%s Encoding cancelled. Stopped ffmpeg process.\n Make sure to clean up the created file\n", X)
+10 -16
View File
@@ -27,10 +27,10 @@ type ffmpegProcessStart struct {
type filesStatMsg struct { type filesStatMsg struct {
fileCount int fileCount int
files []string files []File
} }
func (m *model) statFiles() tea.Msg { func (m *Model) statFiles() tea.Msg {
if m.IsDirectory { if m.IsDirectory {
entries, _ := os.ReadDir(m.Path) entries, _ := os.ReadDir(m.Path)
@@ -53,7 +53,7 @@ func (m *model) statFiles() tea.Msg {
} }
m.FileCount++ m.FileCount++
m.Files = append(m.Files, fullFilePath) m.Files = append(m.Files, File{Path: fullFilePath})
} }
if len(m.Files) == 0 { if len(m.Files) == 0 {
@@ -61,7 +61,7 @@ func (m *model) statFiles() tea.Msg {
} }
} else { } else {
m.FileCount = 1 m.FileCount = 1
m.Files = append(m.Files, m.Path) m.Files = append(m.Files, File{Path: m.Path, Selected: true})
} }
return filesStatMsg{ return filesStatMsg{
@@ -70,12 +70,6 @@ func (m *model) statFiles() tea.Msg {
} }
} }
type encodingStartedMsg struct{}
func startEncoding() tea.Msg {
return encodingStartedMsg{}
}
type encodeVideoMsg struct{} type encodeVideoMsg struct{}
func encodeVideo() tea.Msg { func encodeVideo() tea.Msg {
@@ -93,7 +87,7 @@ type parsedCfgMsg struct {
dryRun bool dryRun bool
} }
func (m *model) parseConfig(dryRun bool) tea.Cmd { func (m *Model) parseConfig(dryRun bool) tea.Cmd {
m.DryRun = dryRun m.DryRun = dryRun
return func() tea.Msg { return func() tea.Msg {
@@ -105,7 +99,7 @@ func (m *model) parseConfig(dryRun bool) tea.Cmd {
return parsedCfgMsg{ return parsedCfgMsg{
parsedConfig: ParsedConfig{ parsedConfig: ParsedConfig{
DeleteOldVideo: find(m.Config, "Delete old video(s)?").FocusedOption != 0, DeleteOldVideo: find(m.Config, "Delete old video(s)?").FocusedOption != 0,
SkipEncodedVid: find(m.Config, "What should we do about encoded videos?").FocusedOption == 0, IgnoreConflictingName: find(m.Config, "On name conflict?").FocusedOption == 0,
VideoEncoder: vEncoder.Opts[vEncoder.FocusedOption], VideoEncoder: vEncoder.Opts[vEncoder.FocusedOption],
AudioEncoder: aEncoder.Opts[aEncoder.FocusedOption], AudioEncoder: aEncoder.Opts[aEncoder.FocusedOption],
Preset: preset.Opts[preset.FocusedOption], Preset: preset.Opts[preset.FocusedOption],
@@ -120,15 +114,15 @@ type errQuitMsg struct {
msg string msg string
} }
func (m *model) cleanUp() tea.Msg { func (m *Model) cleanUp() tea.Msg {
if len(m.Files) == 0 { if len(m.Files) == 0 {
return tea.Quit() return tea.Quit()
} }
fullFilePath := m.Files[len(m.Files)-1] file := m.Files[len(m.Files)-1]
fileName := filepath.Base(fullFilePath) fileName := filepath.Base(file.Path)
parentDir := filepath.Dir(fullFilePath) parentDir := filepath.Dir(file.Path)
extensionIndex := strings.LastIndex(fileName, ".") extensionIndex := strings.LastIndex(fileName, ".")
newFileName := fileName[:extensionIndex] newFileName := fileName[:extensionIndex]
extension := fileName[extensionIndex:] extension := fileName[extensionIndex:]
+8 -31
View File
@@ -18,7 +18,7 @@ var SupportedAudioEncoders = []string{
var Configs = []Config{ var Configs = []Config{
{Name: "Delete old video(s)?", Opts: []string{"No", "Yes"}, FocusedOption: 1}, {Name: "Delete old video(s)?", Opts: []string{"No", "Yes"}, FocusedOption: 1},
{Name: "What should we do about encoded videos?", Opts: []string{"Skip", "Delete and encode again"}}, {Name: "On name conflict?", Opts: []string{"Ignore", "Overwrite"}},
{Name: "Video Encoder", Opts: []string{"copy"}}, {Name: "Video Encoder", Opts: []string{"copy"}},
{Name: "Audio Encoder", Opts: []string{"None", "copy"}, FocusedOption: 1}, {Name: "Audio Encoder", Opts: []string{"None", "copy"}, FocusedOption: 1},
{Name: "Preset", Opts: []string{"ultrafast", "superfast", "veryfast", "faster", "fast", "medium", "slow", "slower", "veryslow"}, FocusedOption: 4}, {Name: "Preset", Opts: []string{"ultrafast", "superfast", "veryfast", "faster", "fast", "medium", "slow", "slower", "veryslow"}, FocusedOption: 4},
@@ -33,7 +33,7 @@ type Config struct {
type ParsedConfig struct { type ParsedConfig struct {
DeleteOldVideo bool DeleteOldVideo bool
SkipEncodedVid bool IgnoreConflictingName bool
VideoEncoder string VideoEncoder string
AudioEncoder string AudioEncoder string
Preset string Preset string
@@ -51,41 +51,18 @@ func find(cfgs []Config, name string) Config {
panic(err) panic(err)
} }
func filter(cfgs []Config, names ...string) []Config {
filtered := make([]Config, 0)
cfgloop:
for _, cfg := range cfgs {
for _, name := range names {
if cfg.Name == name {
continue cfgloop
}
}
filtered = append(filtered, cfg)
}
return filtered
}
func contains[T comparable](slice []T, elem T) bool {
for _, e := range slice {
if e == elem {
return true
}
}
return false
}
func getVisibleConfigs(cfgs []Config) []Config { func getVisibleConfigs(cfgs []Config) []Config {
parsed := parseConfig(cfgs) parsed := parseConfig(cfgs)
switch parsed.VideoEncoder { switch parsed.VideoEncoder {
case "copy", "librav1e": case "copy", "librav1e":
return filter(cfgs, "Preset", "Constant Rate Factor (CRF)") return filter(cfgs, func(c Config) bool {
return c.Name != "Preset" && c.Name != "Constant Rate Factor (CRF)"
})
case "libvpx-vp9", "libsvtav1": case "libvpx-vp9", "libsvtav1":
return filter(cfgs, "Preset") return filter(cfgs, func(c Config) bool {
return c.Name != "Preset"
})
} }
return cfgs return cfgs
+3 -1
View File
@@ -3,7 +3,9 @@ package main
import "testing" import "testing"
func TestFilterConfigs(t *testing.T) { func TestFilterConfigs(t *testing.T) {
cfgs := filter(Configs, "Preset", "Constant Rate Factor (CRF)") cfgs := filter(Configs, func(c Config) bool {
return c.Name != "Preset" && c.Name != "Constant Rate Factor (CRF)"
})
for _, cfg := range cfgs { for _, cfg := range cfgs {
if cfg.Name == "Preset" { if cfg.Name == "Preset" {
+24 -6
View File
@@ -46,11 +46,29 @@ func main() {
log.Fatal(err) log.Fatal(err)
} }
_, err = p.Run() final, err := p.Run()
if err != nil { if err != nil {
fmt.Println(err)
log.Fatal(err) log.Fatal(err)
} }
finalModel, _ := final.(Model)
if finalModel.DryRun {
for _, file := range finalModel.Files {
parentDir := filepath.Dir(file.Path)
fileName := filepath.Base(file.Path)
extensionIndex := strings.LastIndex(fileName, ".")
newFileName := fileName[:extensionIndex]
extension := fileName[extensionIndex:]
outFileFullPath := filepath.Join(parentDir, newFileName+fmt.Sprintf("_[%s]_[%s]", finalModel.ParsedConfig.VideoEncoder, finalModel.ParsedConfig.AudioEncoder)+extension)
cmd := exec.Command("ffmpeg", buildFFmpegCmdArgs(file.Path, outFileFullPath, finalModel.ParsedConfig)...)
fmt.Println(fmt.Sprintf("%s %s", Checkmark, cmd.String()))
}
}
} }
func buildFFmpegCmdArgs(fullFilePath string, outFullFilePath string, cfg ParsedConfig, additionalArgs ...string) []string { func buildFFmpegCmdArgs(fullFilePath string, outFullFilePath string, cfg ParsedConfig, additionalArgs ...string) []string {
@@ -100,8 +118,8 @@ func buildFFmpegCmdArgs(fullFilePath string, outFullFilePath string, cfg ParsedC
return args return args
} }
func encode(fullFilePath string, fileName string, teaP *tea.Program, cfg ParsedConfig) { func encode(file File, fileName string, teaP *tea.Program, cfg ParsedConfig) {
mType, err := mimetype.DetectFile(fullFilePath) mType, err := mimetype.DetectFile(file.Path)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@@ -110,14 +128,14 @@ func encode(fullFilePath string, fileName string, teaP *tea.Program, cfg ParsedC
log.Fatalf("%s is not a valid video file\n", fileName) log.Fatalf("%s is not a valid video file\n", fileName)
} }
parentDir := filepath.Dir(fullFilePath) parentDir := filepath.Dir(file.Path)
extensionIndex := strings.LastIndex(fileName, ".") extensionIndex := strings.LastIndex(fileName, ".")
newFileName := fileName[:extensionIndex] newFileName := fileName[:extensionIndex]
extension := fileName[extensionIndex:] extension := fileName[extensionIndex:]
newFileFullPath := filepath.Join(parentDir, newFileName+fmt.Sprintf("_[%s]_[%s]", cfg.VideoEncoder, cfg.AudioEncoder)+extension) newFileFullPath := filepath.Join(parentDir, newFileName+fmt.Sprintf("_[%s]_[%s]", cfg.VideoEncoder, cfg.AudioEncoder)+extension)
if _, err := os.Stat(newFileFullPath); err == nil { if _, err := os.Stat(newFileFullPath); err == nil {
if cfg.SkipEncodedVid { if cfg.IgnoreConflictingName {
log.Printf("Skipping \"%s\" because it already exists with the exact same encodings (crf and preset might be different though)", newFileFullPath) log.Printf("Skipping \"%s\" because it already exists with the exact same encodings (crf and preset might be different though)", newFileFullPath)
teaP.Send(finishedEncodingVideo{}) teaP.Send(finishedEncodingVideo{})
return return
@@ -126,7 +144,7 @@ func encode(fullFilePath string, fileName string, teaP *tea.Program, cfg ParsedC
} }
} }
cmdArgs := buildFFmpegCmdArgs(fullFilePath, newFileFullPath, cfg, "-progress", "unix://"+getProgressSocket(fullFilePath, teaP)) cmdArgs := buildFFmpegCmdArgs(file.Path, newFileFullPath, cfg, "-progress", "unix://"+getProgressSocket(file.Path, teaP))
cmd := exec.Command("ffmpeg", cmdArgs...) cmd := exec.Command("ffmpeg", cmdArgs...)
teaP.Send(ffmpegProcessStart{cmd}) teaP.Send(ffmpegProcessStart{cmd})
err = cmd.Run() err = cmd.Run()
+48
View File
@@ -18,6 +18,11 @@ var (
Light: "#1C6DD0", Light: "#1C6DD0",
} }
DisabledColor = lipgloss.AdaptiveColor{
Dark: "#444",
Light: "#AAA",
}
BlurredConfig = lipgloss.NewStyle(). BlurredConfig = lipgloss.NewStyle().
Foreground(PrimaryColor). Foreground(PrimaryColor).
MarginTop(1) MarginTop(1)
@@ -49,6 +54,32 @@ var (
Padding(0, 2). Padding(0, 2).
Align(lipgloss.Center). Align(lipgloss.Center).
Render("Start Encoding!") Render("Start Encoding!")
DisabledStartButton = lipgloss.NewStyle().
Border(lipgloss.NormalBorder()).
BorderForeground(DisabledColor).
Foreground(DisabledColor).
MarginTop(1).
Padding(0, 2).
Align(lipgloss.Center).
Render("Start Encoding!")
FocusedNextButton = lipgloss.NewStyle().
Border(lipgloss.NormalBorder()).
BorderForeground(AccentColor).
Foreground(AccentColor).
MarginTop(1).
Padding(0, 2).
Align(lipgloss.Center).
Bold(true).
Render("Next")
BlurredNextButton = lipgloss.NewStyle().
Border(lipgloss.NormalBorder()).
BorderForeground(PrimaryColor).
Foreground(PrimaryColor).
MarginTop(1).
Padding(0, 2).
Align(lipgloss.Center).
Render("Next")
FocusedDryRunButton = lipgloss.NewStyle(). FocusedDryRunButton = lipgloss.NewStyle().
Border(lipgloss.NormalBorder()). Border(lipgloss.NormalBorder()).
@@ -67,4 +98,21 @@ var (
Padding(0, 2). Padding(0, 2).
Align(lipgloss.Center). Align(lipgloss.Center).
Render("Print FFmpeg command and exit") Render("Print FFmpeg command and exit")
FocusedSelectAllButton = lipgloss.NewStyle().
Border(lipgloss.NormalBorder()).
BorderForeground(AccentColor).
Foreground(AccentColor).
MarginTop(1).
Padding(0, 2).
Align(lipgloss.Center).
Bold(true)
BlurredSelectAllButton = lipgloss.NewStyle().
Border(lipgloss.NormalBorder()).
BorderForeground(PrimaryColor).
Foreground(PrimaryColor).
MarginTop(1).
Padding(0, 2).
Align(lipgloss.Center).
Bold(true)
) )
+43
View File
@@ -0,0 +1,43 @@
package main
func contains[T comparable](slice []T, elem T) bool {
for _, e := range slice {
if e == elem {
return true
}
}
return false
}
func every[T any](slice []T, pred func(elem T) bool) bool {
for _, e := range slice {
if !pred(e) {
return false
}
}
return true
}
func anyOf[T any](slice []T, pred func(elem T) bool) bool {
for _, e := range slice {
if pred(e) {
return true
}
}
return false
}
func filter[T any](slice []T, pred func(elem T) bool) []T {
filtered := make([]T, 0)
for _, e := range slice {
if pred(e) {
filtered = append(filtered, e)
}
}
return filtered
}