From 2f9995d8f1cb60491947c5aac5ed3e7c8844e525 Mon Sep 17 00:00:00 2001 From: Stu Leak Date: Sat, 20 Dec 2025 19:55:13 -0500 Subject: [PATCH] Add configurable temp directory with SSD hint --- DONE.md | 1 + TODO.md | 1 + internal/convert/ffmpeg.go | 2 +- internal/utils/utils.go | 23 +++++++++++++++++ main.go | 51 ++++++++++++++++++++++++++++++++------ 5 files changed, 70 insertions(+), 8 deletions(-) diff --git a/DONE.md b/DONE.md index c1e2bb5..77097ce 100644 --- a/DONE.md +++ b/DONE.md @@ -833,6 +833,7 @@ This file tracks completed features, fixes, and milestones. - ✅ Hide quality presets when bitrate mode is not CRF - ✅ Snippet UI now shows Convert Snippet + batch + options with context-sensitive controls - ✅ Reduced module video pane minimum sizes to allow GNOME window snapping +- ✅ Added cache/temp directory setting with SSD recommendation and override - ✅ Snippet defaults now use conversion settings (not Match Source) - ✅ Stabilized video seeking and embedded rendering - ✅ Improved player window positioning diff --git a/TODO.md b/TODO.md index 208d376..10e74e2 100644 --- a/TODO.md +++ b/TODO.md @@ -66,6 +66,7 @@ This file tracks upcoming features, improvements, and known issues. - Quality presets hidden when bitrate mode is not CRF - Snippet UI rearranged into Convert Snippet / Batch / Options with context-sensitive visibility - Reduce module video pane min sizes to allow GNOME snapping + - Cache/temp directory setting with SSD recommendation *Last Updated: 2025-12-20* diff --git a/internal/convert/ffmpeg.go b/internal/convert/ffmpeg.go index 7f592e2..d072675 100644 --- a/internal/convert/ffmpeg.go +++ b/internal/convert/ffmpeg.go @@ -252,7 +252,7 @@ func ProbeVideo(path string) (*VideoSource, error) { // Extract embedded cover art if present if coverArtStreamIndex >= 0 { - coverPath := filepath.Join(os.TempDir(), fmt.Sprintf("videotools-embedded-cover-%d.png", time.Now().UnixNano())) + coverPath := filepath.Join(utils.TempDir(), fmt.Sprintf("videotools-embedded-cover-%d.png", time.Now().UnixNano())) extractCmd := exec.CommandContext(ctx, FFmpegPath, "-i", path, "-map", fmt.Sprintf("0:%d", coverArtStreamIndex), diff --git a/internal/utils/utils.go b/internal/utils/utils.go index 10d0c81..3c9e798 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -8,6 +8,7 @@ import ( "path/filepath" "strconv" "strings" + "sync/atomic" "unicode/utf8" "fyne.io/fyne/v2" @@ -271,3 +272,25 @@ func LoadAppIcon() fyne.Resource { logging.Debug(logging.CatUI, "no app icon found in search paths") return nil } + +var tempDirOverride atomic.Value + +// SetTempDir overrides the app temp directory (empty string resets to system temp). +func SetTempDir(path string) { + trimmed := strings.TrimSpace(path) + if trimmed == "" { + tempDirOverride.Store("") + return + } + tempDirOverride.Store(trimmed) +} + +// TempDir returns the app temp directory, falling back to the system temp dir. +func TempDir() string { + if v := tempDirOverride.Load(); v != nil { + if s, ok := v.(string); ok && s != "" { + return s + } + } + return os.TempDir() +} diff --git a/main.go b/main.go index 4ba3b8e..d72bd2d 100644 --- a/main.go +++ b/main.go @@ -489,6 +489,7 @@ type convertConfig struct { AspectHandling string OutputAspect string AspectUserSet bool // Tracks if user explicitly set OutputAspect + TempDir string // Optional temp/cache directory override } func (c convertConfig) OutputFile() string { @@ -553,6 +554,7 @@ func defaultConvertConfig() convertConfig { AspectHandling: "Auto", OutputAspect: "Source", AspectUserSet: false, + TempDir: "", } } @@ -1792,7 +1794,7 @@ func (s *appState) showBenchmark() { logging.Debug(logging.CatSystem, "detected hardware for benchmark: %s", hwInfo.Summary()) // Create benchmark suite - tmpDir := filepath.Join(os.TempDir(), "videotools-benchmark") + tmpDir := filepath.Join(utils.TempDir(), "videotools-benchmark") _ = os.MkdirAll(tmpDir, 0o755) suite := benchmark.NewSuite(platformConfig.FFmpegPath, tmpDir) @@ -3079,7 +3081,7 @@ func (s *appState) executeMergeJob(ctx context.Context, job *queue.Job, progress return fmt.Errorf("need at least two clips to merge") } - tmpDir := os.TempDir() + tmpDir := utils.TempDir() listFile, err := os.CreateTemp(tmpDir, "vt-merge-list-*.txt") if err != nil { return err @@ -4935,6 +4937,7 @@ func runGUI() { } else if !errors.Is(err, os.ErrNotExist) { logging.Debug(logging.CatSystem, "failed to load persisted convert config: %v", err) } + utils.SetTempDir(state.convert.TempDir) // Initialize conversion history if historyCfg, err := loadHistoryConfig(); err == nil { @@ -5809,6 +5812,33 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject { settingsInfoLabel := widget.NewLabel("Settings persist across videos. Change them anytime to affect all subsequent videos.") settingsInfoLabel.Alignment = fyne.TextAlignCenter + cacheDirLabel := widget.NewLabelWithStyle("Cache/Temp Directory", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}) + cacheDirEntry := widget.NewEntry() + cacheDirEntry.SetPlaceHolder("System temp (recommended SSD)") + cacheDirEntry.SetText(state.convert.TempDir) + cacheDirHint := widget.NewLabel("Use an SSD for best performance. Leave blank to use system temp.") + cacheDirHint.Wrapping = fyne.TextWrapWord + cacheDirEntry.OnChanged = func(val string) { + state.convert.TempDir = strings.TrimSpace(val) + utils.SetTempDir(state.convert.TempDir) + } + cacheBrowseBtn := widget.NewButton("Browse...", func() { + dialog.ShowFolderOpen(func(uri fyne.ListableURI, err error) { + if err != nil || uri == nil { + return + } + cacheDirEntry.SetText(uri.Path()) + state.convert.TempDir = uri.Path() + utils.SetTempDir(state.convert.TempDir) + }, state.window) + }) + cacheUseSystemBtn := widget.NewButton("Use System Temp", func() { + cacheDirEntry.SetText("") + state.convert.TempDir = "" + utils.SetTempDir("") + }) + cacheUseSystemBtn.Importance = widget.LowImportance + resetSettingsBtn := widget.NewButton("Reset to Defaults", func() { if resetConvertDefaults != nil { resetConvertDefaults() @@ -5818,6 +5848,11 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject { settingsContent := container.NewVBox( settingsInfoLabel, + widget.NewSeparator(), + cacheDirLabel, + container.NewBorder(nil, nil, nil, cacheBrowseBtn, cacheDirEntry), + cacheUseSystemBtn, + cacheDirHint, resetSettingsBtn, ) settingsContent.Hide() @@ -6993,6 +7028,8 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject { audioCodecSelect.SetSelected(state.convert.AudioCodec) audioBitrateSelect.SetSelected(state.convert.AudioBitrate) audioChannelsSelect.SetSelected(state.convert.AudioChannels) + cacheDirEntry.SetText(state.convert.TempDir) + utils.SetTempDir(state.convert.TempDir) inverseCheck.SetChecked(state.convert.InverseTelecine) inverseHint.SetText(state.convert.InverseAutoNotes) coverLabel.SetText(state.convert.CoverLabel()) @@ -7913,7 +7950,7 @@ Metadata: %s`, defer cancel() // Generate preview at 10 seconds into the video - previewPath := filepath.Join(os.TempDir(), fmt.Sprintf("deinterlace_preview_%d.png", time.Now().Unix())) + previewPath := filepath.Join(utils.TempDir(), fmt.Sprintf("deinterlace_preview_%d.png", time.Now().Unix())) err := detector.GenerateComparisonPreview(ctx, state.source.Path, 10.0, previewPath) fyne.CurrentApp().Driver().DoFromGoroutine(func() { @@ -8760,7 +8797,7 @@ func (s *appState) showFrameManual(path string, img *canvas.Image) { func (s *appState) captureCoverFromCurrent() (string, error) { // If we have a play session active, capture the current playing frame if s.playSess != nil && s.playSess.img != nil && s.playSess.img.Image != nil { - dest := filepath.Join(os.TempDir(), fmt.Sprintf("videotools-cover-%d.png", time.Now().UnixNano())) + dest := filepath.Join(utils.TempDir(), fmt.Sprintf("videotools-cover-%d.png", time.Now().UnixNano())) f, err := os.Create(dest) if err != nil { return "", err @@ -8780,7 +8817,7 @@ func (s *appState) captureCoverFromCurrent() (string, error) { if err != nil { return "", err } - dest := filepath.Join(os.TempDir(), fmt.Sprintf("videotools-cover-%d.png", time.Now().UnixNano())) + dest := filepath.Join(utils.TempDir(), fmt.Sprintf("videotools-cover-%d.png", time.Now().UnixNano())) if err := os.WriteFile(dest, data, 0o644); err != nil { return "", err } @@ -8792,7 +8829,7 @@ func (s *appState) importCoverImage(path string) (string, error) { if err != nil { return "", err } - dest := filepath.Join(os.TempDir(), fmt.Sprintf("videotools-cover-import-%d%s", time.Now().UnixNano(), filepath.Ext(path))) + dest := filepath.Join(utils.TempDir(), fmt.Sprintf("videotools-cover-import-%d%s", time.Now().UnixNano(), filepath.Ext(path))) if err := os.WriteFile(dest, data, 0o644); err != nil { return "", err } @@ -11089,7 +11126,7 @@ func probeVideo(path string) (*videoSource, error) { // Extract embedded cover art if present if coverArtStreamIndex >= 0 { - coverPath := filepath.Join(os.TempDir(), fmt.Sprintf("videotools-embedded-cover-%d.png", time.Now().UnixNano())) + coverPath := filepath.Join(utils.TempDir(), fmt.Sprintf("videotools-embedded-cover-%d.png", time.Now().UnixNano())) extractCmd := exec.CommandContext(ctx, platformConfig.FFmpegPath, "-i", path, "-map", fmt.Sprintf("0:%d", coverArtStreamIndex),