316 lines
7.1 KiB
Go
316 lines
7.1 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()
|
|
}
|
|
|
|
func (a *App) WindowMinimise() {
|
|
if a.ctx != nil {
|
|
runtime.WindowMinimise(a.ctx)
|
|
}
|
|
}
|
|
|
|
func (a *App) WindowToggleMaximise() {
|
|
if a.ctx != nil {
|
|
runtime.WindowToggleMaximise(a.ctx)
|
|
}
|
|
}
|
|
|
|
func (a *App) WindowClose() {
|
|
if a.ctx != nil {
|
|
runtime.Quit(a.ctx)
|
|
}
|
|
}
|
|
|
|
func (a *App) WindowIsMaximised() bool {
|
|
if a.ctx != nil {
|
|
return runtime.WindowIsMaximised(a.ctx)
|
|
}
|
|
return false
|
|
}
|