Files
convert/app.go
T
2026-11-12 18:04:53 +03:00

291 lines
6.7 KiB
Go

package main
import (
"context"
"fmt"
"os/exec"
"path/filepath"
goruntime "runtime"
"strings"
"sync"
"convert/internal/config"
"convert/internal/convert"
"convert/internal/media"
"convert/internal/preview"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
type AppInfo struct {
Version string `json:"version"`
Binaries media.Binaries `json:"binaries"`
Config config.AppConfig `json:"config"`
InitialFile string `json:"initialFile"`
}
type LoadedVideo struct {
Info *media.VideoInfo `json:"info"`
VideoID string `json:"videoId"`
PreviewURL string `json:"previewUrl"`
ProxyURL string `json:"proxyUrl"`
ProxyMergedURL string `json:"proxyMergedUrl"`
ThumbBaseURL string `json:"thumbBaseUrl"`
DefaultOutputPath string `json:"defaultOutputPath"`
}
type App struct {
ctx context.Context
initialFile string
cfg config.AppConfig
bins media.Binaries
previewSrv *preview.Server
runner *convert.Runner
mu sync.RWMutex
currentInfo *media.VideoInfo
}
func NewApp(initialFile string) *App {
cfg := config.Load()
bins := media.FindBinaries(cfg.FFmpegPath, cfg.FFprobePath)
return &App{
initialFile: initialFile,
cfg: cfg,
bins: bins,
runner: convert.NewRunner(),
}
}
func (a *App) startup(ctx context.Context) {
a.ctx = ctx
srv, err := preview.Start(a.bins.FFmpeg)
if err == nil {
a.previewSrv = srv
}
}
func (a *App) shutdown(_ context.Context) {
if a.runner != nil {
a.runner.Cancel()
}
if a.previewSrv != nil {
a.previewSrv.Close()
}
}
func (a *App) GetAppInfo() AppInfo {
a.mu.RLock()
defer a.mu.RUnlock()
return AppInfo{
Version: Version,
Binaries: a.bins,
Config: a.cfg,
InitialFile: a.initialFile,
}
}
func (a *App) RefreshBinaries() media.Binaries {
a.mu.Lock()
defer a.mu.Unlock()
a.bins = media.FindBinaries(a.cfg.FFmpegPath, a.cfg.FFprobePath)
if a.previewSrv != nil {
a.previewSrv.SetFFmpeg(a.bins.FFmpeg)
}
return a.bins
}
func (a *App) SaveConfig(cfg config.AppConfig) (AppInfo, error) {
if err := config.Save(cfg); err != nil {
return AppInfo{}, err
}
a.mu.Lock()
a.cfg = cfg
a.bins = media.FindBinaries(cfg.FFmpegPath, cfg.FFprobePath)
if a.previewSrv != nil {
a.previewSrv.SetFFmpeg(a.bins.FFmpeg)
}
a.mu.Unlock()
return a.GetAppInfo(), nil
}
func (a *App) OpenVideoDialog() (string, error) {
return runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
Title: "Select Video File",
Filters: []runtime.FileFilter{
{
DisplayName: "Video Files (*.mp4, *.mkv, *.mov, *.avi, *.webm, *.m4v, *.ts, *.flv)",
Pattern: "*.mp4;*.mkv;*.mov;*.avi;*.webm;*.m4v;*.ts;*.flv;*.wmv",
},
{
DisplayName: "All Files (*.*)",
Pattern: "*.*",
},
},
})
}
func (a *App) SaveVideoDialog(defaultPath string) (string, error) {
dir := filepath.Dir(defaultPath)
file := filepath.Base(defaultPath)
return runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
Title: "Save Output Video",
DefaultDirectory: dir,
DefaultFilename: file,
Filters: []runtime.FileFilter{
{
DisplayName: "MP4 Video (*.mp4)",
Pattern: "*.mp4",
},
},
})
}
func (a *App) SelectFFmpegDialog() (AppInfo, error) {
selected, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
Title: "Locate ffmpeg.exe",
Filters: []runtime.FileFilter{
{DisplayName: "ffmpeg executable", Pattern: "ffmpeg.exe;ffmpeg"},
{DisplayName: "All Files", Pattern: "*.*"},
},
})
if err != nil || selected == "" {
return a.GetAppInfo(), err
}
a.mu.Lock()
a.cfg.FFmpegPath = selected
dir := filepath.Dir(selected)
probeName := "ffprobe"
if goruntime.GOOS == "windows" {
probeName = "ffprobe.exe"
}
a.cfg.FFprobePath = filepath.Join(dir, probeName)
_ = config.Save(a.cfg)
a.bins = media.FindBinaries(a.cfg.FFmpegPath, a.cfg.FFprobePath)
if a.previewSrv != nil {
a.previewSrv.SetFFmpeg(a.bins.FFmpeg)
}
a.mu.Unlock()
return a.GetAppInfo(), nil
}
func (a *App) LoadVideo(path string) (*LoadedVideo, error) {
a.mu.RLock()
ffprobeBin := a.bins.FFprobe
suffix := a.cfg.OutputSuffix
a.mu.RUnlock()
info, err := media.Probe(ffprobeBin, path)
if err != nil {
return nil, err
}
a.mu.Lock()
a.currentInfo = info
a.mu.Unlock()
if a.previewSrv == nil {
return nil, fmt.Errorf("preview server is not running")
}
id := a.previewSrv.Register(info)
base := a.previewSrv.BaseURL()
return &LoadedVideo{
Info: info,
VideoID: id,
PreviewURL: fmt.Sprintf("%s/video?id=%s", base, id),
ProxyURL: fmt.Sprintf("%s/proxy?id=%s&merge=0", base, id),
ProxyMergedURL: fmt.Sprintf("%s/proxy?id=%s&merge=1", base, id),
ThumbBaseURL: fmt.Sprintf("%s/thumb?id=%s", base, id),
DefaultOutputPath: convert.DefaultOutputPath(path, suffix),
}, nil
}
func (a *App) PreviewCommand(req convert.Request) (string, error) {
a.mu.RLock()
info := a.currentInfo
a.mu.RUnlock()
if info == nil {
return "", nil
}
built, err := convert.BuildCommand(req, info)
if err != nil {
return "", err
}
parts := make([]string, 0, len(built.Args)+1)
parts = append(parts, "ffmpeg")
for _, arg := range built.Args {
if strings.ContainsAny(arg, " \t") {
parts = append(parts, `"`+arg+`"`)
} else {
parts = append(parts, arg)
}
}
return strings.Join(parts, " "), nil
}
func (a *App) StartConvert(req convert.Request) error {
a.mu.RLock()
info := a.currentInfo
ffmpegBin := a.bins.FFmpeg
a.mu.RUnlock()
if info == nil {
return fmt.Errorf("no video loaded")
}
if ffmpegBin == "" {
return fmt.Errorf("ffmpeg binary not found")
}
if a.runner.IsRunning() {
return fmt.Errorf("conversion already running")
}
built, err := convert.BuildCommand(req, info)
if err != nil {
return err
}
go func() {
err := a.runner.Run(a.ctx, ffmpegBin, built, func(p convert.Progress) {
runtime.EventsEmit(a.ctx, "convert:progress", p)
})
if err != nil {
runtime.EventsEmit(a.ctx, "convert:error", err.Error())
return
}
runtime.EventsEmit(a.ctx, "convert:done", built.OutputPath)
}()
return nil
}
func (a *App) CancelConvert() bool {
return a.runner.Cancel()
}
func (a *App) OpenInExplorer(path string) error {
if path == "" {
return nil
}
if goruntime.GOOS == "windows" {
cmd := exec.Command("explorer", "/select,", filepath.Clean(path))
return cmd.Start()
}
return exec.Command("xdg-open", filepath.Dir(path)).Start()
}
func (a *App) OpenOutputFile(path string) error {
if path == "" {
return nil
}
if goruntime.GOOS == "windows" {
cmd := exec.Command("cmd", "/c", "start", "", filepath.Clean(path))
media.HideCmdWindow(cmd)
return cmd.Start()
}
return exec.Command("xdg-open", path).Start()
}