diff --git a/src/convertor/service.go b/src/convertor/service.go index 5d395b1..8d82a12 100644 --- a/src/convertor/service.go +++ b/src/convertor/service.go @@ -11,11 +11,19 @@ import ( type ServiceContract interface { RunConvert(setting ConvertSetting) error GetTotalDuration(file *File) (float64, error) + GetFFmpegVesrion() (string, error) + GetFFprobeVersion() (string, error) + ChangeFFmpegPath(path string) (bool, error) + ChangeFFprobePath(path string) (bool, error) +} + +type FFPathUtilities struct { + FFmpeg string + FFprobe string } type Service struct { - pathFFmpeg string - pathFFprobe string + ffPathUtilities *FFPathUtilities } type File struct { @@ -33,10 +41,9 @@ type ConvertData struct { totalDuration float64 } -func NewService(pathFFmpeg string, pathFFprobe string) *Service { +func NewService(ffPathUtilities FFPathUtilities) *Service { return &Service{ - pathFFmpeg: pathFFmpeg, - pathFFprobe: pathFFprobe, + ffPathUtilities: &ffPathUtilities, } } @@ -48,7 +55,7 @@ func (s Service) RunConvert(setting ConvertSetting) error { //args := "-n -i " + setting.VideoFileInput.Path + " -c:v libx264 -progress unix://" + setting.SocketPath + " output-file.mp4" //args := "-y -i " + setting.VideoFileInput.Path + " -c:v libx264 -progress unix://" + setting.SocketPath + " output-file.mp4" args := []string{"-y", "-i", setting.VideoFileInput.Path, "-c:v", "libx264", "-progress", "unix://" + setting.SocketPath, "output-file.mp4"} - cmd := exec.Command(s.pathFFmpeg, args...) + cmd := exec.Command(s.ffPathUtilities.FFmpeg, args...) out, err := cmd.CombinedOutput() if err != nil { @@ -64,7 +71,7 @@ func (s Service) RunConvert(setting ConvertSetting) error { func (s Service) GetTotalDuration(file *File) (duration float64, err error) { args := []string{"-v", "error", "-select_streams", "v:0", "-count_packets", "-show_entries", "stream=nb_read_packets", "-of", "csv=p=0", file.Path} - cmd := exec.Command(s.pathFFprobe, args...) + cmd := exec.Command(s.ffPathUtilities.FFprobe, args...) out, err := cmd.CombinedOutput() if err != nil { errString := strings.TrimSpace(string(out)) @@ -75,3 +82,49 @@ func (s Service) GetTotalDuration(file *File) (duration float64, err error) { } return strconv.ParseFloat(strings.TrimSpace(string(out)), 64) } + +func (s Service) GetFFmpegVesrion() (string, error) { + cmd := exec.Command(s.ffPathUtilities.FFmpeg, "-version") + out, err := cmd.CombinedOutput() + if err != nil { + return "", err + } + text := regexp.MustCompile("\r?\n").Split(strings.TrimSpace(string(out)), -1) + return text[0], nil +} + +func (s Service) GetFFprobeVersion() (string, error) { + cmd := exec.Command(s.ffPathUtilities.FFprobe, "-version") + out, err := cmd.CombinedOutput() + if err != nil { + return "", err + } + text := regexp.MustCompile("\r?\n").Split(strings.TrimSpace(string(out)), -1) + return text[0], nil +} + +func (s Service) ChangeFFmpegPath(path string) (bool, error) { + cmd := exec.Command(path, "-version") + out, err := cmd.CombinedOutput() + if err != nil { + return false, err + } + if strings.Contains(strings.TrimSpace(string(out)), "ffmpeg") == false { + return false, nil + } + s.ffPathUtilities.FFmpeg = path + return true, nil +} + +func (s Service) ChangeFFprobePath(path string) (bool, error) { + cmd := exec.Command(path, "-version") + out, err := cmd.CombinedOutput() + if err != nil { + return false, err + } + if strings.Contains(strings.TrimSpace(string(out)), "ffprobe") == false { + return false, nil + } + s.ffPathUtilities.FFprobe = path + return true, nil +} diff --git a/src/data/.gitignore b/src/data/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/src/data/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/src/go.mod b/src/go.mod index 387abeb..2831b7f 100644 --- a/src/go.mod +++ b/src/go.mod @@ -17,7 +17,10 @@ require ( github.com/go-text/typesetting v0.0.0-20230616162802-9c17dd34aa4a // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gopherjs/gopherjs v1.17.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e // indirect + github.com/mattn/go-sqlite3 v1.14.19 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect @@ -30,5 +33,7 @@ require ( golang.org/x/sys v0.13.0 // indirect golang.org/x/text v0.13.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + gorm.io/driver/sqlite v1.5.4 // indirect + gorm.io/gorm v1.25.5 // indirect honnef.co/go/js/dom v0.0.0-20210725211120-f030747120f2 // indirect ) diff --git a/src/go.sum b/src/go.sum index 1de087d..0fdad10 100644 --- a/src/go.sum +++ b/src/go.sum @@ -191,6 +191,10 @@ github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/J github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= @@ -206,6 +210,8 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI= +github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -647,6 +653,12 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/sqlite v1.5.4 h1:IqXwXi8M/ZlPzH/947tn5uik3aYQslP9BVveoax0nV0= +gorm.io/driver/sqlite v1.5.4/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4= +gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55 h1:sC1Xj4TYrLqg1n3AN10w871An7wJM0gzgcm8jkIkECQ= +gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= +gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= +gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= honnef.co/go/js/dom v0.0.0-20210725211120-f030747120f2 h1:oomkgU6VaQDsV6qZby2uz1Lap0eXmku8+2em3A/l700= honnef.co/go/js/dom v0.0.0-20210725211120-f030747120f2/go.mod h1:sUMDUKNB2ZcVjt92UnLy3cdGs+wDAcrPdV3JP6sVgA4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/src/handler/convertor.go b/src/handler/convertor.go index 7fc8a18..cd13541 100644 --- a/src/handler/convertor.go +++ b/src/handler/convertor.go @@ -1,8 +1,9 @@ package handler import ( + "errors" "ffmpegGui/convertor" - myError "ffmpegGui/error" + "ffmpegGui/setting" "fmt" "fyne.io/fyne/v2/widget" "log" @@ -11,31 +12,39 @@ import ( "os" "path" "regexp" + "runtime" "strconv" "strings" "time" ) type ConvertorHandler struct { - convertorService convertor.ServiceContract - convertorView convertor.ViewContract - errorView myError.ViewContract + convertorService convertor.ServiceContract + convertorView convertor.ViewContract + settingView setting.ViewContract + settingRepository setting.RepositoryContract } func NewConvertorHandler( convertorService convertor.ServiceContract, convertorView convertor.ViewContract, - errorView myError.ViewContract, + settingView setting.ViewContract, + settingRepository setting.RepositoryContract, ) *ConvertorHandler { return &ConvertorHandler{ convertorService, convertorView, - errorView, + settingView, + settingRepository, } } func (h ConvertorHandler) GetConvertor() { - h.convertorView.Main(h.runConvert, h.getSockPath) + if h.checkingFFPathUtilities() == true { + h.convertorView.Main(h.runConvert, h.getSockPath) + return + } + h.settingView.SelectFFPath(h.saveSettingFFPath) } func (h ConvertorHandler) getSockPath(file *convertor.File, progressbar *widget.ProgressBar) (string, error) { @@ -104,3 +113,68 @@ func (h ConvertorHandler) runConvert(setting convertor.HandleConvertSetting) err func (h ConvertorHandler) getTotalDuration(file *convertor.File) (float64, error) { return h.convertorService.GetTotalDuration(file) } + +func (h ConvertorHandler) checkingFFPathUtilities() bool { + if h.checkingFFPath() == true { + return true + } + + var pathsToFF []convertor.FFPathUtilities + if runtime.GOOS == "windows" { + pathsToFF = []convertor.FFPathUtilities{{"ffmpeg/bin/ffmpeg.exe", "ffmpeg/bin/ffprobe.exe"}} + } else { + pathsToFF = []convertor.FFPathUtilities{{"ffmpeg/bin/ffmpeg", "ffmpeg/bin/ffprobe"}, {"ffmpeg", "ffprobe"}} + } + for _, item := range pathsToFF { + ffmpegChecking, _ := h.convertorService.ChangeFFmpegPath(item.FFmpeg) + if ffmpegChecking == false { + continue + } + ffprobeChecking, _ := h.convertorService.ChangeFFprobePath(item.FFprobe) + if ffprobeChecking == false { + continue + } + ffmpegEntity := setting.Setting{Code: "ffmpeg", Value: item.FFmpeg} + h.settingRepository.Create(ffmpegEntity) + ffprobeEntity := setting.Setting{Code: "ffprobe", Value: item.FFprobe} + h.settingRepository.Create(ffprobeEntity) + return true + } + + return false +} + +func (h ConvertorHandler) saveSettingFFPath(ffmpegPath string, ffprobePath string) error { + ffmpegChecking, _ := h.convertorService.ChangeFFmpegPath(ffmpegPath) + if ffmpegChecking == false { + return errors.New("Это не FFmpeg") + } + + ffprobeChecking, _ := h.convertorService.ChangeFFprobePath(ffprobePath) + if ffprobeChecking == false { + return errors.New("Это не FFprobe") + } + + ffmpegEntity := setting.Setting{Code: "ffmpeg", Value: ffmpegPath} + h.settingRepository.Create(ffmpegEntity) + ffprobeEntity := setting.Setting{Code: "ffprobe", Value: ffprobePath} + h.settingRepository.Create(ffprobeEntity) + + h.GetConvertor() + + return nil +} + +func (h ConvertorHandler) checkingFFPath() bool { + _, err := h.convertorService.GetFFmpegVesrion() + if err != nil { + return false + } + + _, err = h.convertorService.GetFFprobeVersion() + if err != nil { + return false + } + + return true +} diff --git a/src/main.go b/src/main.go index 7cfa2c4..c74719d 100644 --- a/src/main.go +++ b/src/main.go @@ -1,11 +1,18 @@ package main import ( + "errors" "ffmpegGui/convertor" myError "ffmpegGui/error" "ffmpegGui/handler" + "ffmpegGui/migration" + "ffmpegGui/setting" "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" + _ "github.com/mattn/go-sqlite3" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "os" ) const appVersion string = "0.1.0" @@ -17,14 +24,66 @@ func main() { errorView := myError.NewView(w) - pathFFmpeg := "ffmpeg" - pathFFprobe := "ffprobe" + if canCreateFile("data/database") != true { + errorView.PanicError(errors.New("Не смогли создать файл 'database' в папке 'data'")) + w.ShowAndRun() + return + } + + db, err := gorm.Open(sqlite.Open("data/database"), &gorm.Config{}) + if err != nil { + errorView.PanicError(err) + w.ShowAndRun() + return + } + + defer appClose(db) + + err = migration.Run(db) + if err != nil { + errorView.PanicError(err) + w.ShowAndRun() + return + } + + settingRepository := setting.NewRepository(db) + pathFFmpeg, err := settingRepository.GetValue("ffmpeg") + if err != nil && errors.Is(err, gorm.ErrRecordNotFound) == false { + errorView.PanicError(err) + w.ShowAndRun() + return + } + pathFFprobe, err := settingRepository.GetValue("ffprobe") + if err != nil && errors.Is(err, gorm.ErrRecordNotFound) == false { + errorView.PanicError(err) + w.ShowAndRun() + return + } + + ffPathUtilities := convertor.FFPathUtilities{FFmpeg: pathFFmpeg, FFprobe: pathFFprobe} convertorView := convertor.NewView(w) - convertorService := convertor.NewService(pathFFmpeg, pathFFprobe) - mainHandler := handler.NewConvertorHandler(convertorService, convertorView, errorView) + settingView := setting.NewView(w) + convertorService := convertor.NewService(ffPathUtilities) + mainHandler := handler.NewConvertorHandler(convertorService, convertorView, settingView, settingRepository) mainHandler.GetConvertor() w.ShowAndRun() } + +func appClose(db *gorm.DB) { + sqlDB, err := db.DB() + if err == nil { + _ = sqlDB.Close() + } +} + +func canCreateFile(path string) bool { + file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0666) + if err != nil { + return false + } + _ = file.Close() + return true +} diff --git a/src/migration/migration.go b/src/migration/migration.go new file mode 100644 index 0000000..b3a801a --- /dev/null +++ b/src/migration/migration.go @@ -0,0 +1,15 @@ +package migration + +import ( + "ffmpegGui/setting" + "gorm.io/gorm" +) + +func Run(db *gorm.DB) error { + err := db.AutoMigrate(&setting.Setting{}) + if err != nil { + return err + } + + return nil +} diff --git a/src/setting/entity.go b/src/setting/entity.go new file mode 100644 index 0000000..12f9bb2 --- /dev/null +++ b/src/setting/entity.go @@ -0,0 +1,7 @@ +package setting + +type Setting struct { + ID uint `gorm:"primary_key"` + Code string `gorm:"type:varchar(100);uniqueIndex;not null"` + Value string `gorm:"type:text"` +} diff --git a/src/setting/repository.go b/src/setting/repository.go new file mode 100644 index 0000000..065bb60 --- /dev/null +++ b/src/setting/repository.go @@ -0,0 +1,35 @@ +package setting + +import ( + "gorm.io/gorm" +) + +type RepositoryContract interface { + Create(setting Setting) (Setting, error) + GetValue(code string) (value string, err error) +} + +type Repository struct { + db *gorm.DB +} + +func NewRepository(db *gorm.DB) *Repository { + return &Repository{db} +} + +func (r Repository) GetValue(code string) (value string, err error) { + var setting Setting + err = r.db.Where("code = ?", code).First(&setting).Error + if err != nil { + return "", err + } + return setting.Value, err +} + +func (r Repository) Create(setting Setting) (Setting, error) { + err := r.db.Create(&setting).Error + if err != nil { + return setting, err + } + return setting, err +} diff --git a/src/setting/view.go b/src/setting/view.go new file mode 100644 index 0000000..7a85e0d --- /dev/null +++ b/src/setting/view.go @@ -0,0 +1,98 @@ +package setting + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/dialog" + "fyne.io/fyne/v2/widget" + "image/color" + "net/url" +) + +type ViewContract interface { + SelectFFPath(func(ffmpegPath string, ffprobePath string) error) +} + +type View struct { + w fyne.Window +} + +func NewView(w fyne.Window) *View { + return &View{w} +} + +func (v View) SelectFFPath(save func(ffmpegPath string, ffprobePath string) error) { + errorMessage := canvas.NewText("", color.RGBA{255, 0, 0, 255}) + errorMessage.TextSize = 16 + errorMessage.TextStyle = fyne.TextStyle{Bold: true} + + ffmpegPath, buttonFFmpeg, buttonFFmpegMessage := v.getButtonSelectFile() + ffprobePath, buttonFFprobe, buttonFFprobeMessage := v.getButtonSelectFile() + + link := widget.NewHyperlink("https://ffmpeg.org/download.html", &url.URL{ + Scheme: "https", + Host: "ffmpeg.org", + Path: "download.html", + }) + + form := &widget.Form{ + Items: []*widget.FormItem{ + {Text: "Скачать можно от сюда", Widget: link}, + {Text: "Путь к ffmpeg:", Widget: buttonFFmpeg}, + {Widget: buttonFFmpegMessage}, + {Text: "Путь к ffprobe:", Widget: buttonFFprobe}, + {Widget: buttonFFprobeMessage}, + {Widget: errorMessage}, + }, + SubmitText: "Сохранить", + OnSubmit: func() { + err := save(string(*ffmpegPath), string(*ffprobePath)) + if err != nil { + errorMessage.Text = err.Error() + } + }, + } + v.w.SetContent(widget.NewCard("Укажите путь к FFmpeg и к FFprobe", "", container.NewVBox(form))) +} + +func (v View) getButtonSelectFile() (filePath *string, button *widget.Button, buttonMessage *canvas.Text) { + path := "" + filePath = &path + + buttonMessage = canvas.NewText("", color.RGBA{255, 0, 0, 255}) + buttonMessage.TextSize = 16 + buttonMessage.TextStyle = fyne.TextStyle{Bold: true} + + button = widget.NewButton("выбрать", func() { + fileDialog := dialog.NewFileOpen( + func(r fyne.URIReadCloser, err error) { + if err != nil { + buttonMessage.Text = err.Error() + setStringErrorStyle(buttonMessage) + return + } + if r == nil { + return + } + + path = r.URI().Path() + + buttonMessage.Text = r.URI().Path() + setStringSuccessStyle(buttonMessage) + }, v.w) + fileDialog.Show() + }) + + return +} + +func setStringErrorStyle(text *canvas.Text) { + text.Color = color.RGBA{255, 0, 0, 255} + text.Refresh() +} + +func setStringSuccessStyle(text *canvas.Text) { + text.Color = color.RGBA{49, 127, 114, 255} + text.Refresh() +}