mirror of
https://github.com/IllusionMan1212/ffui.git
synced 2026-10-24 18:12:35 +00:00
Compare commits
5
Commits
fb390bbcbe
...
2951ea391f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2951ea391f
|
||
|
|
95bedb8ee7
|
||
|
|
85ebb57885
|
||
|
|
1d456b9863
|
||
|
|
2e1d175e98
|
@@ -1,5 +1,6 @@
|
||||
- [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
|
||||
- This will of course have a select all checkbox for quickly encoding all the files in a dir.
|
||||
- [-] when passing in a directory, list all video files in that directory and have the user choose which ones they want encoded
|
||||
- [ ] 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.
|
||||
might not do this.
|
||||
|
||||
@@ -21,22 +21,22 @@ var X = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF2233")).Render("✖️
|
||||
|
||||
const (
|
||||
None Screen = iota
|
||||
Files
|
||||
Cfg
|
||||
Files
|
||||
Main
|
||||
)
|
||||
|
||||
type File struct {
|
||||
Name string
|
||||
Encode bool
|
||||
Path string
|
||||
Selected bool
|
||||
}
|
||||
|
||||
type model struct {
|
||||
type Model struct {
|
||||
IsDirectory bool
|
||||
Path string
|
||||
CurrentFileName string
|
||||
FileCount int
|
||||
Files []string
|
||||
Files []File
|
||||
Spinner spinner.Model
|
||||
SingleFileProgressBar progress.Model
|
||||
SingleFileProgress float64
|
||||
@@ -51,13 +51,16 @@ type model struct {
|
||||
Config []Config
|
||||
VisibleConfig []Config
|
||||
FocusIndex int
|
||||
ChoiceIndex int
|
||||
ParsedConfig ParsedConfig
|
||||
DryRun bool
|
||||
ErrQuit bool
|
||||
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 = spinner.Dot
|
||||
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(),
|
||||
CurrentFileName: fileInfo.Name(),
|
||||
Path: absolutePath,
|
||||
FileCount: 0,
|
||||
Files: make([]string, 0),
|
||||
Files: make([]File, 0),
|
||||
Screen: Cfg,
|
||||
Spinner: s,
|
||||
SingleFileProgressBar: progress.New(progress.WithGradient("#1010ff", "#00ff00")),
|
||||
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 i := range m.Config {
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
switch msg := msg.(type) {
|
||||
@@ -128,32 +132,61 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
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
|
||||
}
|
||||
|
||||
switch m.Screen {
|
||||
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
|
||||
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 "enter", " ":
|
||||
if m.FocusIndex == 0 && key == "enter" {
|
||||
selectAll := !every(m.Files, func(e File) bool { return e.Selected })
|
||||
|
||||
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":
|
||||
m.FocusIndex = 0
|
||||
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":
|
||||
if key == "up" || key == "shift+tab" || key == "k" {
|
||||
m.FocusIndex--
|
||||
@@ -161,10 +194,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.FocusIndex++
|
||||
}
|
||||
|
||||
if m.FocusIndex >= len(m.Files) {
|
||||
if m.FocusIndex >= len(m.Files)+1 {
|
||||
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", " ":
|
||||
// parse config and switch to main screen if we're focused on the start button
|
||||
if m.FocusIndex == len(m.VisibleConfig) {
|
||||
if m.ChoiceIndex == 0 {
|
||||
return m, m.parseConfig(false)
|
||||
}
|
||||
// parse config, print it and exit
|
||||
if m.FocusIndex == len(m.VisibleConfig)+1 {
|
||||
} else if m.ChoiceIndex == 1 {
|
||||
return m, m.parseConfig(true)
|
||||
}
|
||||
}
|
||||
case "g":
|
||||
m.FocusIndex = 0
|
||||
case "G":
|
||||
m.FocusIndex = len(m.VisibleConfig) + 1
|
||||
m.FocusIndex = len(m.VisibleConfig)
|
||||
case "tab", "shift+tab", "up", "down", "j", "k":
|
||||
if key == "up" || key == "shift+tab" || key == "k" {
|
||||
m.FocusIndex--
|
||||
@@ -195,10 +228,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.FocusIndex++
|
||||
}
|
||||
|
||||
if m.FocusIndex >= len(m.VisibleConfig)+2 {
|
||||
if m.FocusIndex > len(m.VisibleConfig) {
|
||||
m.FocusIndex = 0
|
||||
} else if m.FocusIndex < 0 {
|
||||
m.FocusIndex = len(m.VisibleConfig) + 1
|
||||
m.FocusIndex = len(m.VisibleConfig)
|
||||
}
|
||||
case "left", "right", "h", "l":
|
||||
// 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.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:
|
||||
m.Quitting = true
|
||||
return m, tea.Quit
|
||||
case parsedCfgMsg:
|
||||
m.Screen = Main
|
||||
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:
|
||||
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)
|
||||
}
|
||||
case encodingStartedMsg:
|
||||
if m.DryRun {
|
||||
return m, tea.Sequence(tea.ExitAltScreen, gracefullyQuit)
|
||||
}
|
||||
|
||||
return m, encodeVideo
|
||||
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() {
|
||||
fullFilePath := m.Files[len(m.Files)-1]
|
||||
encode(fullFilePath, filepath.Base(fullFilePath), m.Program, m.ParsedConfig)
|
||||
file := m.Files[len(m.Files)-1]
|
||||
encode(file, filepath.Base(file.Path), m.Program, m.ParsedConfig)
|
||||
}()
|
||||
|
||||
return m, nil
|
||||
@@ -263,8 +314,8 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
case finishedEncodingVideo:
|
||||
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])
|
||||
m.CurrentFileName = fmt.Sprintf("Deleting: %s", filepath.Base(m.Files[len(m.Files)-1].Path))
|
||||
os.Remove(m.Files[len(m.Files)-1].Path)
|
||||
}
|
||||
|
||||
m.Files = m.Files[:len(m.Files)-1]
|
||||
@@ -311,7 +362,7 @@ func parseConfig(cfg []Config) ParsedConfig {
|
||||
|
||||
return ParsedConfig{
|
||||
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],
|
||||
AudioEncoder: aEncoder.Opts[aEncoder.FocusedOption],
|
||||
Preset: preset.Opts[preset.FocusedOption],
|
||||
@@ -319,25 +370,58 @@ func parseConfig(cfg []Config) ParsedConfig {
|
||||
}
|
||||
}
|
||||
|
||||
func FilesScreenView(m model) string {
|
||||
view := ""
|
||||
func FilesScreenView(m Model) string {
|
||||
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
|
||||
// encoding.
|
||||
for i, file := range m.Files {
|
||||
if m.FocusIndex == i {
|
||||
view += fmt.Sprintf(FocusedConfig.Render("[%s] %s"), " ", filepath.Base(file))
|
||||
allSelected := every(m.Files, func(e File) bool { return e.Selected })
|
||||
noneSelected := !anyOf(m.Files, func(e File) bool { return e.Selected })
|
||||
|
||||
if allSelected {
|
||||
selectAllBtnText = "Deselect All"
|
||||
} 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
|
||||
}
|
||||
|
||||
func CfgScreenView(m model) string {
|
||||
func CfgScreenView(m Model) string {
|
||||
view := ""
|
||||
|
||||
for i, cfg := range m.VisibleConfig {
|
||||
@@ -362,27 +446,39 @@ func CfgScreenView(m model) string {
|
||||
}
|
||||
}
|
||||
|
||||
if m.FocusIndex == len(m.VisibleConfig) {
|
||||
view += FocusedStartButton
|
||||
var startButton string
|
||||
var dryRunButton string
|
||||
|
||||
if m.FocusIndex == len(m.VisibleConfig) && m.ChoiceIndex == 0 {
|
||||
if m.IsDirectory {
|
||||
startButton = FocusedNextButton
|
||||
} else {
|
||||
view += BlurredStartButton
|
||||
startButton = FocusedStartButton
|
||||
}
|
||||
} else {
|
||||
if m.IsDirectory {
|
||||
startButton = BlurredNextButton
|
||||
} else {
|
||||
startButton = BlurredStartButton
|
||||
}
|
||||
}
|
||||
|
||||
if m.FocusIndex == len(m.VisibleConfig)+1 {
|
||||
view += FocusedDryRunButton
|
||||
if m.FocusIndex == len(m.VisibleConfig) && m.ChoiceIndex == 1 {
|
||||
dryRunButton = FocusedDryRunButton
|
||||
} else {
|
||||
view += BlurredDryRunButton
|
||||
dryRunButton = BlurredDryRunButton
|
||||
}
|
||||
|
||||
view += lipgloss.JoinHorizontal(0, startButton, dryRunButton)
|
||||
view += "\n"
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
func MainScreenView(m model) string {
|
||||
func MainScreenView(m Model) string {
|
||||
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",
|
||||
m.Spinner.View(),
|
||||
m.FileCount-len(m.Files),
|
||||
@@ -425,28 +521,13 @@ func formatEstimate(estimate int) string {
|
||||
return fmt.Sprintf("%ds", estimate)
|
||||
}
|
||||
|
||||
func (m model) View() string {
|
||||
if m.Quitting {
|
||||
func (m Model) View() string {
|
||||
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 ""
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s %s\n", Checkmark, sb.String())
|
||||
} else {
|
||||
if m.Quitting {
|
||||
return fmt.Sprintf("%s %d/%d files encoded\n", Checkmark, m.FileCount-len(m.Files), m.FileCount)
|
||||
}
|
||||
} else if m.Cancelled {
|
||||
// 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)
|
||||
|
||||
@@ -27,10 +27,10 @@ type ffmpegProcessStart struct {
|
||||
|
||||
type filesStatMsg struct {
|
||||
fileCount int
|
||||
files []string
|
||||
files []File
|
||||
}
|
||||
|
||||
func (m *model) statFiles() tea.Msg {
|
||||
func (m *Model) statFiles() tea.Msg {
|
||||
if m.IsDirectory {
|
||||
entries, _ := os.ReadDir(m.Path)
|
||||
|
||||
@@ -53,7 +53,7 @@ func (m *model) statFiles() tea.Msg {
|
||||
}
|
||||
|
||||
m.FileCount++
|
||||
m.Files = append(m.Files, fullFilePath)
|
||||
m.Files = append(m.Files, File{Path: fullFilePath})
|
||||
}
|
||||
|
||||
if len(m.Files) == 0 {
|
||||
@@ -61,7 +61,7 @@ func (m *model) statFiles() tea.Msg {
|
||||
}
|
||||
} else {
|
||||
m.FileCount = 1
|
||||
m.Files = append(m.Files, m.Path)
|
||||
m.Files = append(m.Files, File{Path: m.Path, Selected: true})
|
||||
}
|
||||
|
||||
return filesStatMsg{
|
||||
@@ -70,12 +70,6 @@ func (m *model) statFiles() tea.Msg {
|
||||
}
|
||||
}
|
||||
|
||||
type encodingStartedMsg struct{}
|
||||
|
||||
func startEncoding() tea.Msg {
|
||||
return encodingStartedMsg{}
|
||||
}
|
||||
|
||||
type encodeVideoMsg struct{}
|
||||
|
||||
func encodeVideo() tea.Msg {
|
||||
@@ -93,7 +87,7 @@ type parsedCfgMsg struct {
|
||||
dryRun bool
|
||||
}
|
||||
|
||||
func (m *model) parseConfig(dryRun bool) tea.Cmd {
|
||||
func (m *Model) parseConfig(dryRun bool) tea.Cmd {
|
||||
m.DryRun = dryRun
|
||||
|
||||
return func() tea.Msg {
|
||||
@@ -105,7 +99,7 @@ func (m *model) parseConfig(dryRun bool) tea.Cmd {
|
||||
return parsedCfgMsg{
|
||||
parsedConfig: ParsedConfig{
|
||||
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],
|
||||
AudioEncoder: aEncoder.Opts[aEncoder.FocusedOption],
|
||||
Preset: preset.Opts[preset.FocusedOption],
|
||||
@@ -120,15 +114,15 @@ type errQuitMsg struct {
|
||||
msg string
|
||||
}
|
||||
|
||||
func (m *model) cleanUp() tea.Msg {
|
||||
func (m *Model) cleanUp() tea.Msg {
|
||||
if len(m.Files) == 0 {
|
||||
return tea.Quit()
|
||||
}
|
||||
|
||||
fullFilePath := m.Files[len(m.Files)-1]
|
||||
fileName := filepath.Base(fullFilePath)
|
||||
file := m.Files[len(m.Files)-1]
|
||||
fileName := filepath.Base(file.Path)
|
||||
|
||||
parentDir := filepath.Dir(fullFilePath)
|
||||
parentDir := filepath.Dir(file.Path)
|
||||
extensionIndex := strings.LastIndex(fileName, ".")
|
||||
newFileName := fileName[:extensionIndex]
|
||||
extension := fileName[extensionIndex:]
|
||||
|
||||
@@ -18,7 +18,7 @@ var SupportedAudioEncoders = []string{
|
||||
|
||||
var Configs = []Config{
|
||||
{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: "Audio Encoder", Opts: []string{"None", "copy"}, FocusedOption: 1},
|
||||
{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 {
|
||||
DeleteOldVideo bool
|
||||
SkipEncodedVid bool
|
||||
IgnoreConflictingName bool
|
||||
VideoEncoder string
|
||||
AudioEncoder string
|
||||
Preset string
|
||||
@@ -51,41 +51,18 @@ func find(cfgs []Config, name string) Config {
|
||||
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 {
|
||||
parsed := parseConfig(cfgs)
|
||||
|
||||
switch parsed.VideoEncoder {
|
||||
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":
|
||||
return filter(cfgs, "Preset")
|
||||
return filter(cfgs, func(c Config) bool {
|
||||
return c.Name != "Preset"
|
||||
})
|
||||
}
|
||||
|
||||
return cfgs
|
||||
|
||||
+3
-1
@@ -3,7 +3,9 @@ package main
|
||||
import "testing"
|
||||
|
||||
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 {
|
||||
if cfg.Name == "Preset" {
|
||||
|
||||
@@ -46,11 +46,29 @@ func main() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = p.Run()
|
||||
final, err := p.Run()
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(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 {
|
||||
@@ -100,8 +118,8 @@ func buildFFmpegCmdArgs(fullFilePath string, outFullFilePath string, cfg ParsedC
|
||||
return args
|
||||
}
|
||||
|
||||
func encode(fullFilePath string, fileName string, teaP *tea.Program, cfg ParsedConfig) {
|
||||
mType, err := mimetype.DetectFile(fullFilePath)
|
||||
func encode(file File, fileName string, teaP *tea.Program, cfg ParsedConfig) {
|
||||
mType, err := mimetype.DetectFile(file.Path)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
parentDir := filepath.Dir(fullFilePath)
|
||||
parentDir := filepath.Dir(file.Path)
|
||||
extensionIndex := strings.LastIndex(fileName, ".")
|
||||
newFileName := fileName[:extensionIndex]
|
||||
extension := fileName[extensionIndex:]
|
||||
newFileFullPath := filepath.Join(parentDir, newFileName+fmt.Sprintf("_[%s]_[%s]", cfg.VideoEncoder, cfg.AudioEncoder)+extension)
|
||||
|
||||
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)
|
||||
teaP.Send(finishedEncodingVideo{})
|
||||
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...)
|
||||
teaP.Send(ffmpegProcessStart{cmd})
|
||||
err = cmd.Run()
|
||||
|
||||
@@ -18,6 +18,11 @@ var (
|
||||
Light: "#1C6DD0",
|
||||
}
|
||||
|
||||
DisabledColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#444",
|
||||
Light: "#AAA",
|
||||
}
|
||||
|
||||
BlurredConfig = lipgloss.NewStyle().
|
||||
Foreground(PrimaryColor).
|
||||
MarginTop(1)
|
||||
@@ -49,6 +54,32 @@ var (
|
||||
Padding(0, 2).
|
||||
Align(lipgloss.Center).
|
||||
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().
|
||||
Border(lipgloss.NormalBorder()).
|
||||
@@ -67,4 +98,21 @@ var (
|
||||
Padding(0, 2).
|
||||
Align(lipgloss.Center).
|
||||
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)
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user