From e288d5efbbba93e2b3c5273353c43280d9ccc93b Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sun, 14 Jan 2024 16:16:26 +0600 Subject: [PATCH 01/15] Changing the name on the license. --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index d08a830..0fce2f8 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 kor-elf +Copyright (c) 2024 Leonid Nikitin (kor-elf) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -- 2.45.2 From 5a4c96020129967cc257afc738f508261d11a108 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sun, 14 Jan 2024 16:31:07 +0600 Subject: [PATCH 02/15] Working prototype. A simple working graphical interface with which you can transcode any video to mp4. --- src/convertor/service.go | 78 +++++ src/convertor/view.go | 121 +++++++ src/error/view.go | 26 ++ src/go.mod | 34 ++ src/go.sum | 661 +++++++++++++++++++++++++++++++++++++++ src/handler/convertor.go | 112 +++++++ src/main.go | 30 ++ 7 files changed, 1062 insertions(+) create mode 100644 src/convertor/service.go create mode 100644 src/convertor/view.go create mode 100644 src/error/view.go create mode 100644 src/go.mod create mode 100644 src/go.sum create mode 100644 src/handler/convertor.go create mode 100644 src/main.go diff --git a/src/convertor/service.go b/src/convertor/service.go new file mode 100644 index 0000000..eb809da --- /dev/null +++ b/src/convertor/service.go @@ -0,0 +1,78 @@ +package convertor + +import ( + "os/exec" + "strconv" + "strings" +) + +type ServiceContract interface { + RunConvert(setting ConvertSetting) error + GetTotalDuration(file File) (float64, error) +} + +type Service struct { + pathFFmpeg string + pathFFprobe string +} + +type File struct { + Path string + Name string + Ext string +} + +type ConvertSetting struct { + VideoFileInput File + SocketPath string +} + +type ConvertData struct { + totalDuration float64 +} + +func NewService(pathFFmpeg string, pathFFprobe string) *Service { + return &Service{ + pathFFmpeg: pathFFmpeg, + pathFFprobe: pathFFprobe, + } +} + +func (s Service) RunConvert(setting ConvertSetting) error { + //args := strings.Split("-report -n -c:v libx264", " ") + //args := strings.Split("-n -c:v libx264", " ") + //args = append(args, "-progress", "unix://"+setting.SocketPath, "-i", setting.VideoFileInput.Path, "file-out.mp4") + //args := "-report -n -i " + setting.VideoFileInput.Path + " -c:v libx264 -progress unix://" + setting.SocketPath + " output-file.mp4" + //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" + cmd := exec.Command("ffmpeg", strings.Split(args, " ")...) + + //stderr, _ := cmd.StdoutPipe() + err := cmd.Start() + if err != nil { + return err + } + + //scanner := bufio.NewScanner(stderr) + ////scanner.Split(bufio.ScanWords) + //for scanner.Scan() { + // m := scanner.Text() + // fmt.Println(m) + //} + err = cmd.Wait() + if err != nil { + return err + } + + return nil +} + +func (s Service) GetTotalDuration(file File) (duration float64, err error) { + args := "-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, strings.Split(args, " ")...) + out, err := cmd.CombinedOutput() + if err != nil { + return 0, err + } + return strconv.ParseFloat(strings.TrimSpace(string(out)), 64) +} diff --git a/src/convertor/view.go b/src/convertor/view.go new file mode 100644 index 0000000..d31ea87 --- /dev/null +++ b/src/convertor/view.go @@ -0,0 +1,121 @@ +package convertor + +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" +) + +type ViewContract interface { + Main( + runConvert func(setting HandleConvertSetting) error, + getSocketPath func(File, *widget.ProgressBar) (string, error), + ) +} + +type View struct { + w fyne.Window +} + +type HandleConvertSetting struct { + VideoFileInput File + SocketPath string +} + +func NewView(w fyne.Window) *View { + return &View{w} +} + +func (v View) Main( + runConvert func(setting HandleConvertSetting) error, + getSocketPath func(File, *widget.ProgressBar) (string, error), +) { + var fileInput File + var form *widget.Form + + fileVideoForConversionMessage := canvas.NewText("", color.RGBA{255, 0, 0, 255}) + fileVideoForConversionMessage.TextSize = 16 + fileVideoForConversionMessage.TextStyle = fyne.TextStyle{Bold: true} + + conversionMessage := canvas.NewText("", color.RGBA{255, 0, 0, 255}) + conversionMessage.TextSize = 16 + conversionMessage.TextStyle = fyne.TextStyle{Bold: true} + + progress := widget.NewProgressBar() + progress.Hide() + + fileVideoForConversion := widget.NewButton("выбрать", func() { + fileDialog := dialog.NewFileOpen( + func(r fyne.URIReadCloser, err error) { + if err != nil { + fileVideoForConversionMessage.Text = err.Error() + setStringErrorStyle(fileVideoForConversionMessage) + return + } + + if r == nil { + return + } + + fileInput = File{ + Path: r.URI().Path(), + Name: r.URI().Name(), + Ext: r.URI().Extension(), + } + fileVideoForConversionMessage.Text = r.URI().Path() + setStringSuccessStyle(fileVideoForConversionMessage) + + form.Enable() + }, v.w) + fileDialog.Show() + }) + + form = &widget.Form{ + Items: []*widget.FormItem{ + {Text: "Файл для ковертации:", Widget: fileVideoForConversion}, + {Widget: fileVideoForConversionMessage}, + }, + SubmitText: "Конвертировать", + OnSubmit: func() { + fileVideoForConversion.Disable() + form.Disable() + + socketPath, err := getSocketPath(fileInput, progress) + + if err != nil { + conversionMessage.Text = err.Error() + setStringErrorStyle(conversionMessage) + fileVideoForConversion.Enable() + form.Enable() + } + + setting := HandleConvertSetting{ + VideoFileInput: fileInput, + SocketPath: socketPath, + } + err = runConvert(setting) + if err != nil { + conversionMessage.Text = err.Error() + setStringErrorStyle(conversionMessage) + } + fileVideoForConversion.Enable() + form.Enable() + }, + } + + v.w.SetContent(widget.NewCard("Конвертор видео файлов", "", container.NewVBox(form, conversionMessage, progress))) + form.Disable() +} + +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() +} diff --git a/src/error/view.go b/src/error/view.go new file mode 100644 index 0000000..df3fc7d --- /dev/null +++ b/src/error/view.go @@ -0,0 +1,26 @@ +package error + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/widget" +) + +type ViewContract interface { + PanicError(err error) +} + +type View struct { + w fyne.Window +} + +func NewView(w fyne.Window) *View { + return &View{w} +} + +func (v View) PanicError(err error) { + v.w.SetContent(container.NewVBox( + widget.NewLabel("Произошла ошибка!"), + widget.NewLabel("Ошибка: "+err.Error()), + )) +} diff --git a/src/go.mod b/src/go.mod new file mode 100644 index 0000000..387abeb --- /dev/null +++ b/src/go.mod @@ -0,0 +1,34 @@ +module ffmpegGui + +go 1.21 + +require ( + fyne.io/fyne/v2 v2.4.3 // indirect + fyne.io/systray v1.10.1-0.20231115130155-104f5ef7839e // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fredbi/uri v1.0.0 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/fyne-io/gl-js v0.0.0-20220119005834-d2da28d9ccfe // indirect + github.com/fyne-io/glfw-js v0.0.0-20220120001248-ee7290d23504 // indirect + github.com/fyne-io/image v0.0.0-20220602074514-4956b0afb3d2 // indirect + github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 // indirect + github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect + github.com/go-text/render v0.0.0-20230619120952-35bccb6164b8 // indirect + 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/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e // 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 + github.com/stretchr/testify v1.8.4 // indirect + github.com/tevino/abool v1.2.0 // indirect + github.com/yuin/goldmark v1.5.5 // indirect + golang.org/x/image v0.11.0 // indirect + golang.org/x/mobile v0.0.0-20230531173138-3c911d8e3eda // indirect + golang.org/x/net v0.17.0 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/text v0.13.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + honnef.co/go/js/dom v0.0.0-20210725211120-f030747120f2 // indirect +) diff --git a/src/go.sum b/src/go.sum new file mode 100644 index 0000000..1de087d --- /dev/null +++ b/src/go.sum @@ -0,0 +1,661 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +fyne.io/fyne/v2 v2.4.3 h1:v2wncjEAcwXZ8UNmTCWTGL9+sGyPc5RuzBvM96GcC78= +fyne.io/fyne/v2 v2.4.3/go.mod h1:1h3BKxmQYRJlr2g+RGVxedzr6vLVQ/AJmFWcF9CJnoQ= +fyne.io/systray v1.10.1-0.20231115130155-104f5ef7839e h1:Hvs+kW2VwCzNToF3FmnIAzmivNgrclwPgoUdVSrjkP8= +fyne.io/systray v1.10.1-0.20231115130155-104f5ef7839e/go.mod h1:oM2AQqGJ1AMo4nNqZFYU8xYygSBZkW2hmdJ7n4yjedE= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fredbi/uri v1.0.0 h1:s4QwUAZ8fz+mbTsukND+4V5f+mJ/wjaTokwstGUAemg= +github.com/fredbi/uri v1.0.0/go.mod h1:1xC40RnIOGCaQzswaOvrzvG/3M3F0hyDVb3aO/1iGy0= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/fyne-io/gl-js v0.0.0-20220119005834-d2da28d9ccfe h1:A/wiwvQ0CAjPkuJytaD+SsXkPU0asQ+guQEIg1BJGX4= +github.com/fyne-io/gl-js v0.0.0-20220119005834-d2da28d9ccfe/go.mod h1:d4clgH0/GrRwWjRzJJQXxT/h1TyuNSfF/X64zb/3Ggg= +github.com/fyne-io/glfw-js v0.0.0-20220120001248-ee7290d23504 h1:+31CdF/okdokeFNoy9L/2PccG3JFidQT3ev64/r4pYU= +github.com/fyne-io/glfw-js v0.0.0-20220120001248-ee7290d23504/go.mod h1:gLRWYfYnMA9TONeppRSikMdXlHQ97xVsPojddUv3b/E= +github.com/fyne-io/image v0.0.0-20220602074514-4956b0afb3d2 h1:hnLq+55b7Zh7/2IRzWCpiTcAvjv/P8ERF+N7+xXbZhk= +github.com/fyne-io/image v0.0.0-20220602074514-4956b0afb3d2/go.mod h1:eO7W361vmlPOrykIg+Rsh1SZ3tQBaOsfzZhsIOb/Lm0= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 h1:zDw5v7qm4yH7N8C8uWd+8Ii9rROdgWxQuGoJ9WDXxfk= +github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211213063430-748e38ca8aec/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b h1:GgabKamyOYguHqHjSkDACcgoPIz3w0Dis/zJ1wyHHHU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-text/render v0.0.0-20230619120952-35bccb6164b8 h1:VkKnvzbvHqgEfm351rfr8Uclu5fnwq8HP2ximUzJsBM= +github.com/go-text/render v0.0.0-20230619120952-35bccb6164b8/go.mod h1:h29xCucjNsDcYb7+0rJokxVwYAq+9kQ19WiFuBKkYtc= +github.com/go-text/typesetting v0.0.0-20230616162802-9c17dd34aa4a h1:VjN8ttdfklC0dnAdKbZqGNESdERUxtE3l8a/4Grgarc= +github.com/go-text/typesetting v0.0.0-20230616162802-9c17dd34aa4a/go.mod h1:evDBbvNR/KaVFZ2ZlDSOWWXIUKq0wCOEtzLxRM8SG3k= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20211219123610-ec9572f70e60/go.mod h1:cz9oNYuRUWGdHmLF2IodMLkAhcPtXeULvcBNagUrxTI= +github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= +github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= +github.com/goxjs/gl v0.0.0-20210104184919-e3fafc6f8f2a/go.mod h1:dy/f2gjY09hwVfIyATps4G2ai7/hLwLkc5TrPqONuXY= +github.com/goxjs/glfw v0.0.0-20191126052801-d2efb5f20838/go.mod h1:oS8P8gVOT4ywTcjV6wZlOU4GuVFQ8F5328KY3MJ79CY= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +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/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= +github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e h1:LvL4XsI70QxOGHed6yhQtAU34Kx3Qq2wwBzGFKY8zKk= +github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +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/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= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= +github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= +github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE= +github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q= +github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ= +github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tevino/abool v1.2.0 h1:heAkClL8H6w+mK5md9dzsuohKeXHUpY7Vw0ZCKW+huA= +github.com/tevino/abool v1.2.0/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark v1.5.5 h1:IJznPe8wOzfIKETmMkd06F8nXkmlhaHqFRM9l1hAGsU= +github.com/yuin/goldmark v1.5.5/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.11.0 h1:ds2RoQvBvYTiJkwpSFDwCcDFNX7DqjL2WsUgTNk0Ooo= +golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mobile v0.0.0-20211207041440-4e6c2922fdee/go.mod h1:pe2sM7Uk+2Su1y7u/6Z8KJ24D7lepUjFZbhFOrmDfuQ= +golang.org/x/mobile v0.0.0-20230531173138-3c911d8e3eda h1:O+EUvnBNPwI4eLthn8W5K+cS8zQZfgTABPLNm6Bna34= +golang.org/x/mobile v0.0.0-20230531173138-3c911d8e3eda/go.mod h1:aAjjkJNdrh3PMckS4B10TGS2nag27cbKR1y2BpUxsiY= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.8-0.20211022200916-316ba0b74098/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +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= +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= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/src/handler/convertor.go b/src/handler/convertor.go new file mode 100644 index 0000000..2fe519d --- /dev/null +++ b/src/handler/convertor.go @@ -0,0 +1,112 @@ +package handler + +import ( + "ffmpegGui/convertor" + myError "ffmpegGui/error" + "fmt" + "fyne.io/fyne/v2/widget" + "log" + "math/rand" + "net" + "os" + "path" + "regexp" + "strconv" + "strings" + "time" +) + +type ConvertorHandler struct { + convertorService convertor.ServiceContract + convertorView convertor.ViewContract + errorView myError.ViewContract +} + +func NewConvertorHandler( + convertorService convertor.ServiceContract, + convertorView convertor.ViewContract, + errorView myError.ViewContract, +) *ConvertorHandler { + return &ConvertorHandler{ + convertorService, + convertorView, + errorView, + } +} + +func (h ConvertorHandler) GetConvertor() { + h.convertorView.Main(h.runConvert, h.getSockPath) +} + +func (h ConvertorHandler) getSockPath(file convertor.File, progressbar *widget.ProgressBar) (string, error) { + totalDuration, err := h.getTotalDuration(file) + + if err != nil { + return "", err + } + progressbar.Value = 0 + progressbar.Max = totalDuration + progressbar.Show() + progressbar.Refresh() + + rand.Seed(time.Now().Unix()) + sockFileName := path.Join(os.TempDir(), fmt.Sprintf("%d_sock", rand.Int())) + l, err := net.Listen("unix", sockFileName) + if err != nil { + return "", err + } + + go func() { + re := regexp.MustCompile(`frame=(\d+)`) + fd, err := l.Accept() + if err != nil { + log.Fatal("accept error:", err) + } + buf := make([]byte, 16) + data := "" + progress := 0.0 + progressMessage := "" + for { + _, err := fd.Read(buf) + if err != nil { + return + } + data += string(buf) + a := re.FindAllStringSubmatch(data, -1) + cp := "" + if len(a) > 0 && len(a[len(a)-1]) > 0 { + c, _ := strconv.Atoi(a[len(a)-1][len(a[len(a)-1])-1]) + progress = float64(c) + cp = fmt.Sprintf("%.2f", float64(c)/totalDuration*100) + } + if strings.Contains(data, "progress=end") { + cp = "done" + progress = totalDuration + } + if cp == "" { + cp = ".0" + } + if cp != progressMessage { + progressbar.Value = progress + progressbar.Refresh() + progressMessage = cp + fmt.Println("progress: ", progressMessage) + } + } + }() + + return sockFileName, nil +} + +func (h ConvertorHandler) runConvert(setting convertor.HandleConvertSetting) error { + return h.convertorService.RunConvert( + convertor.ConvertSetting{ + VideoFileInput: setting.VideoFileInput, + SocketPath: setting.SocketPath, + }, + ) +} + +func (h ConvertorHandler) getTotalDuration(file convertor.File) (float64, error) { + return h.convertorService.GetTotalDuration(file) +} diff --git a/src/main.go b/src/main.go new file mode 100644 index 0000000..7cfa2c4 --- /dev/null +++ b/src/main.go @@ -0,0 +1,30 @@ +package main + +import ( + "ffmpegGui/convertor" + myError "ffmpegGui/error" + "ffmpegGui/handler" + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/app" +) + +const appVersion string = "0.1.0" + +func main() { + a := app.New() + w := a.NewWindow("GUI FFMpeg!") + w.Resize(fyne.Size{800, 600}) + + errorView := myError.NewView(w) + + pathFFmpeg := "ffmpeg" + pathFFprobe := "ffprobe" + + convertorView := convertor.NewView(w) + convertorService := convertor.NewService(pathFFmpeg, pathFFprobe) + mainHandler := handler.NewConvertorHandler(convertorService, convertorView, errorView) + + mainHandler.GetConvertor() + + w.ShowAndRun() +} -- 2.45.2 From 97dd0f4b3209e638305d06a9ab633df6b6dbf66e Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sun, 14 Jan 2024 17:25:35 +0600 Subject: [PATCH 03/15] Refactor getSockPath. Removed extra code. --- src/handler/convertor.go | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/handler/convertor.go b/src/handler/convertor.go index 2fe519d..26c578c 100644 --- a/src/handler/convertor.go +++ b/src/handler/convertor.go @@ -65,7 +65,6 @@ func (h ConvertorHandler) getSockPath(file convertor.File, progressbar *widget.P buf := make([]byte, 16) data := "" progress := 0.0 - progressMessage := "" for { _, err := fd.Read(buf) if err != nil { @@ -73,24 +72,19 @@ func (h ConvertorHandler) getSockPath(file convertor.File, progressbar *widget.P } data += string(buf) a := re.FindAllStringSubmatch(data, -1) - cp := "" if len(a) > 0 && len(a[len(a)-1]) > 0 { - c, _ := strconv.Atoi(a[len(a)-1][len(a[len(a)-1])-1]) + c, err := strconv.Atoi(a[len(a)-1][len(a[len(a)-1])-1]) + if err != nil { + return + } progress = float64(c) - cp = fmt.Sprintf("%.2f", float64(c)/totalDuration*100) } if strings.Contains(data, "progress=end") { - cp = "done" progress = totalDuration } - if cp == "" { - cp = ".0" - } - if cp != progressMessage { + if progressbar.Value != progress { progressbar.Value = progress progressbar.Refresh() - progressMessage = cp - fmt.Println("progress: ", progressMessage) } } }() -- 2.45.2 From dddbfa65bc800e5f4581e6e40a671547d53a0cd4 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Mon, 15 Jan 2024 20:28:02 +0600 Subject: [PATCH 04/15] Refactor package "convertor" "Main" method. Moved the button for selecting a video file into a separate function. --- src/convertor/service.go | 13 ++-- src/convertor/view.go | 127 +++++++++++++++++++++++---------------- src/handler/convertor.go | 4 +- 3 files changed, 85 insertions(+), 59 deletions(-) diff --git a/src/convertor/service.go b/src/convertor/service.go index eb809da..ea6bdf4 100644 --- a/src/convertor/service.go +++ b/src/convertor/service.go @@ -1,6 +1,7 @@ package convertor import ( + "errors" "os/exec" "strconv" "strings" @@ -8,7 +9,7 @@ import ( type ServiceContract interface { RunConvert(setting ConvertSetting) error - GetTotalDuration(file File) (float64, error) + GetTotalDuration(file *File) (float64, error) } type Service struct { @@ -23,7 +24,7 @@ type File struct { } type ConvertSetting struct { - VideoFileInput File + VideoFileInput *File SocketPath string } @@ -67,12 +68,12 @@ func (s Service) RunConvert(setting ConvertSetting) error { return nil } -func (s Service) GetTotalDuration(file File) (duration float64, err error) { - args := "-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, strings.Split(args, " ")...) +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...) out, err := cmd.CombinedOutput() if err != nil { - return 0, err + return 0, errors.New(strings.TrimSpace(string(out))) } return strconv.ParseFloat(strings.TrimSpace(string(out)), 64) } diff --git a/src/convertor/view.go b/src/convertor/view.go index d31ea87..1d0dd05 100644 --- a/src/convertor/view.go +++ b/src/convertor/view.go @@ -12,7 +12,7 @@ import ( type ViewContract interface { Main( runConvert func(setting HandleConvertSetting) error, - getSocketPath func(File, *widget.ProgressBar) (string, error), + getSocketPath func(*File, *widget.ProgressBar) (string, error), ) } @@ -21,24 +21,24 @@ type View struct { } type HandleConvertSetting struct { - VideoFileInput File + VideoFileInput *File SocketPath string } +type enableFormConversionStruct struct { + fileVideoForConversion *widget.Button + form *widget.Form +} + func NewView(w fyne.Window) *View { return &View{w} } func (v View) Main( runConvert func(setting HandleConvertSetting) error, - getSocketPath func(File, *widget.ProgressBar) (string, error), + getSocketPath func(*File, *widget.ProgressBar) (string, error), ) { - var fileInput File - var form *widget.Form - - fileVideoForConversionMessage := canvas.NewText("", color.RGBA{255, 0, 0, 255}) - fileVideoForConversionMessage.TextSize = 16 - fileVideoForConversionMessage.TextStyle = fyne.TextStyle{Bold: true} + form := &widget.Form{} conversionMessage := canvas.NewText("", color.RGBA{255, 0, 0, 255}) conversionMessage.TextSize = 16 @@ -47,7 +47,56 @@ func (v View) Main( progress := widget.NewProgressBar() progress.Hide() - fileVideoForConversion := widget.NewButton("выбрать", func() { + fileVideoForConversion, fileVideoForConversionMessage, fileInput := v.getButtonFileVideoForConversion(form, progress, conversionMessage) + + form.Items = []*widget.FormItem{ + {Text: "Файл для ковертации:", Widget: fileVideoForConversion}, + {Widget: fileVideoForConversionMessage}, + } + form.SubmitText = "Конвертировать" + + enableFormConversionStruct := enableFormConversionStruct{ + fileVideoForConversion: fileVideoForConversion, + form: form, + } + + form.OnSubmit = func() { + fileVideoForConversion.Disable() + form.Disable() + + socketPath, err := getSocketPath(fileInput, progress) + + if err != nil { + showConversionMessage(conversionMessage, err) + enableFormConversion(enableFormConversionStruct) + return + } + + setting := HandleConvertSetting{ + VideoFileInput: fileInput, + SocketPath: socketPath, + } + err = runConvert(setting) + if err != nil { + showConversionMessage(conversionMessage, err) + enableFormConversion(enableFormConversionStruct) + return + } + enableFormConversion(enableFormConversionStruct) + } + + v.w.SetContent(widget.NewCard("Конвертор видео файлов", "", container.NewVBox(form, conversionMessage, progress))) + form.Disable() +} + +func (v View) getButtonFileVideoForConversion(form *widget.Form, progress *widget.ProgressBar, conversionMessage *canvas.Text) (*widget.Button, *canvas.Text, *File) { + fileInput := &File{} + + fileVideoForConversionMessage := canvas.NewText("", color.RGBA{255, 0, 0, 255}) + fileVideoForConversionMessage.TextSize = 16 + fileVideoForConversionMessage.TextStyle = fyne.TextStyle{Bold: true} + + button := widget.NewButton("выбрать", func() { fileDialog := dialog.NewFileOpen( func(r fyne.URIReadCloser, err error) { if err != nil { @@ -55,59 +104,25 @@ func (v View) Main( setStringErrorStyle(fileVideoForConversionMessage) return } - if r == nil { return } - fileInput = File{ - Path: r.URI().Path(), - Name: r.URI().Name(), - Ext: r.URI().Extension(), - } + fileInput.Path = r.URI().Path() + fileInput.Name = r.URI().Name() + fileInput.Ext = r.URI().Extension() + fileVideoForConversionMessage.Text = r.URI().Path() setStringSuccessStyle(fileVideoForConversionMessage) form.Enable() + progress.Hide() + conversionMessage.Text = "" }, v.w) fileDialog.Show() }) - form = &widget.Form{ - Items: []*widget.FormItem{ - {Text: "Файл для ковертации:", Widget: fileVideoForConversion}, - {Widget: fileVideoForConversionMessage}, - }, - SubmitText: "Конвертировать", - OnSubmit: func() { - fileVideoForConversion.Disable() - form.Disable() - - socketPath, err := getSocketPath(fileInput, progress) - - if err != nil { - conversionMessage.Text = err.Error() - setStringErrorStyle(conversionMessage) - fileVideoForConversion.Enable() - form.Enable() - } - - setting := HandleConvertSetting{ - VideoFileInput: fileInput, - SocketPath: socketPath, - } - err = runConvert(setting) - if err != nil { - conversionMessage.Text = err.Error() - setStringErrorStyle(conversionMessage) - } - fileVideoForConversion.Enable() - form.Enable() - }, - } - - v.w.SetContent(widget.NewCard("Конвертор видео файлов", "", container.NewVBox(form, conversionMessage, progress))) - form.Disable() + return button, fileVideoForConversionMessage, fileInput } func setStringErrorStyle(text *canvas.Text) { @@ -119,3 +134,13 @@ func setStringSuccessStyle(text *canvas.Text) { text.Color = color.RGBA{49, 127, 114, 255} text.Refresh() } + +func showConversionMessage(conversionMessage *canvas.Text, err error) { + conversionMessage.Text = err.Error() + setStringErrorStyle(conversionMessage) +} + +func enableFormConversion(enableFormConversionStruct enableFormConversionStruct) { + enableFormConversionStruct.fileVideoForConversion.Enable() + enableFormConversionStruct.form.Enable() +} diff --git a/src/handler/convertor.go b/src/handler/convertor.go index 26c578c..7fc8a18 100644 --- a/src/handler/convertor.go +++ b/src/handler/convertor.go @@ -38,7 +38,7 @@ func (h ConvertorHandler) GetConvertor() { h.convertorView.Main(h.runConvert, h.getSockPath) } -func (h ConvertorHandler) getSockPath(file convertor.File, progressbar *widget.ProgressBar) (string, error) { +func (h ConvertorHandler) getSockPath(file *convertor.File, progressbar *widget.ProgressBar) (string, error) { totalDuration, err := h.getTotalDuration(file) if err != nil { @@ -101,6 +101,6 @@ func (h ConvertorHandler) runConvert(setting convertor.HandleConvertSetting) err ) } -func (h ConvertorHandler) getTotalDuration(file convertor.File) (float64, error) { +func (h ConvertorHandler) getTotalDuration(file *convertor.File) (float64, error) { return h.convertorService.GetTotalDuration(file) } -- 2.45.2 From 176189c9d08c7ef03bf8482cc64348a7f561a49c Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Tue, 16 Jan 2024 00:04:50 +0600 Subject: [PATCH 05/15] Adjusted display of error text when an error occurs during video conversion. --- src/convertor/service.go | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/src/convertor/service.go b/src/convertor/service.go index ea6bdf4..41c05cb 100644 --- a/src/convertor/service.go +++ b/src/convertor/service.go @@ -3,6 +3,7 @@ package convertor import ( "errors" "os/exec" + "regexp" "strconv" "strings" ) @@ -46,23 +47,12 @@ func (s Service) RunConvert(setting ConvertSetting) error { //args := "-report -n -i " + setting.VideoFileInput.Path + " -c:v libx264 -progress unix://" + setting.SocketPath + " output-file.mp4" //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" - cmd := exec.Command("ffmpeg", strings.Split(args, " ")...) + cmd := exec.Command(s.pathFFmpeg, strings.Split(args, " ")...) - //stderr, _ := cmd.StdoutPipe() - err := cmd.Start() + out, err := cmd.CombinedOutput() if err != nil { - return err - } - - //scanner := bufio.NewScanner(stderr) - ////scanner.Split(bufio.ScanWords) - //for scanner.Scan() { - // m := scanner.Text() - // fmt.Println(m) - //} - err = cmd.Wait() - if err != nil { - return err + errStringArr := regexp.MustCompile("\r?\n").Split(strings.TrimSpace(string(out)), -1) + return errors.New(errStringArr[len(errStringArr)-1]) } return nil -- 2.45.2 From 1070b796cc0013c5b669e1432e097e7c7dce3575 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Tue, 16 Jan 2024 00:15:45 +0600 Subject: [PATCH 06/15] Fix RunConvert. Fixed an error converting videos with a file name that has spaces. --- src/convertor/service.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/convertor/service.go b/src/convertor/service.go index 41c05cb..424b0a4 100644 --- a/src/convertor/service.go +++ b/src/convertor/service.go @@ -46,8 +46,9 @@ func (s Service) RunConvert(setting ConvertSetting) error { //args = append(args, "-progress", "unix://"+setting.SocketPath, "-i", setting.VideoFileInput.Path, "file-out.mp4") //args := "-report -n -i " + setting.VideoFileInput.Path + " -c:v libx264 -progress unix://" + setting.SocketPath + " output-file.mp4" //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" - cmd := exec.Command(s.pathFFmpeg, strings.Split(args, " ")...) + //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...) out, err := cmd.CombinedOutput() if err != nil { -- 2.45.2 From 10aa917c24a1e4565416bf3de65477dca7d47778 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Tue, 16 Jan 2024 18:08:50 +0600 Subject: [PATCH 07/15] I made it so that an error would be displayed if ffmpeg was not found. --- src/convertor/service.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/convertor/service.go b/src/convertor/service.go index 424b0a4..5d395b1 100644 --- a/src/convertor/service.go +++ b/src/convertor/service.go @@ -53,7 +53,10 @@ func (s Service) RunConvert(setting ConvertSetting) error { out, err := cmd.CombinedOutput() if err != nil { errStringArr := regexp.MustCompile("\r?\n").Split(strings.TrimSpace(string(out)), -1) - return errors.New(errStringArr[len(errStringArr)-1]) + if len(errStringArr) > 1 { + return errors.New(errStringArr[len(errStringArr)-1]) + } + return err } return nil @@ -64,7 +67,11 @@ func (s Service) GetTotalDuration(file *File) (duration float64, err error) { cmd := exec.Command(s.pathFFprobe, args...) out, err := cmd.CombinedOutput() if err != nil { - return 0, errors.New(strings.TrimSpace(string(out))) + errString := strings.TrimSpace(string(out)) + if len(errString) > 1 { + return 0, errors.New(errString) + } + return 0, err } return strconv.ParseFloat(strings.TrimSpace(string(out)), 64) } -- 2.45.2 From 5051c65ec608daf6eb6e17c6c01f8a75940ff56b Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Thu, 18 Jan 2024 01:39:20 +0600 Subject: [PATCH 08/15] Fix path to ffmpeg and ffprobe. Added the ability to select the path to ffmpeg and ffprobe. --- src/convertor/service.go | 67 +++++++++++++++++++++++--- src/data/.gitignore | 2 + src/go.mod | 5 ++ src/go.sum | 12 +++++ src/handler/convertor.go | 88 +++++++++++++++++++++++++++++++--- src/main.go | 67 ++++++++++++++++++++++++-- src/migration/migration.go | 15 ++++++ src/setting/entity.go | 7 +++ src/setting/repository.go | 35 ++++++++++++++ src/setting/view.go | 98 ++++++++++++++++++++++++++++++++++++++ 10 files changed, 378 insertions(+), 18 deletions(-) create mode 100644 src/data/.gitignore create mode 100644 src/migration/migration.go create mode 100644 src/setting/entity.go create mode 100644 src/setting/repository.go create mode 100644 src/setting/view.go 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() +} -- 2.45.2 From ebc8832d4d54b351c77ab6f613b57d8cb54ea94e Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Thu, 18 Jan 2024 20:23:23 +0600 Subject: [PATCH 09/15] Made it possible to choose where to save. --- src/convertor/service.go | 18 +++++------ src/convertor/view.go | 65 ++++++++++++++++++++++++++++++++++++---- src/handler/convertor.go | 30 +++++++++++-------- src/helper/helper.go | 10 +++++++ src/main.go | 6 ++-- 5 files changed, 99 insertions(+), 30 deletions(-) create mode 100644 src/helper/helper.go diff --git a/src/convertor/service.go b/src/convertor/service.go index 8d82a12..e1828a2 100644 --- a/src/convertor/service.go +++ b/src/convertor/service.go @@ -33,8 +33,10 @@ type File struct { } type ConvertSetting struct { - VideoFileInput *File - SocketPath string + VideoFileInput *File + VideoFileOut *File + SocketPath string + OverwriteOutputFiles bool } type ConvertData struct { @@ -48,13 +50,11 @@ func NewService(ffPathUtilities FFPathUtilities) *Service { } func (s Service) RunConvert(setting ConvertSetting) error { - //args := strings.Split("-report -n -c:v libx264", " ") - //args := strings.Split("-n -c:v libx264", " ") - //args = append(args, "-progress", "unix://"+setting.SocketPath, "-i", setting.VideoFileInput.Path, "file-out.mp4") - //args := "-report -n -i " + setting.VideoFileInput.Path + " -c:v libx264 -progress unix://" + setting.SocketPath + " output-file.mp4" - //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"} + overwriteOutputFiles := "-n" + if setting.OverwriteOutputFiles == true { + overwriteOutputFiles = "-y" + } + args := []string{overwriteOutputFiles, "-i", setting.VideoFileInput.Path, "-c:v", "libx264", "-progress", "unix://" + setting.SocketPath, setting.VideoFileOut.Path} cmd := exec.Command(s.ffPathUtilities.FFmpeg, args...) out, err := cmd.CombinedOutput() diff --git a/src/convertor/view.go b/src/convertor/view.go index 1d0dd05..e5325dd 100644 --- a/src/convertor/view.go +++ b/src/convertor/view.go @@ -1,6 +1,7 @@ package convertor import ( + "errors" "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" @@ -21,12 +22,15 @@ type View struct { } type HandleConvertSetting struct { - VideoFileInput *File - SocketPath string + VideoFileInput *File + DirectoryForSave string + SocketPath string + OverwriteOutputFiles bool } type enableFormConversionStruct struct { fileVideoForConversion *widget.Button + buttonForSelectedDir *widget.Button form *widget.Form } @@ -45,23 +49,40 @@ func (v View) Main( conversionMessage.TextStyle = fyne.TextStyle{Bold: true} progress := widget.NewProgressBar() - progress.Hide() fileVideoForConversion, fileVideoForConversionMessage, fileInput := v.getButtonFileVideoForConversion(form, progress, conversionMessage) + buttonForSelectedDir, buttonForSelectedDirMessage, pathToSaveDirectory := v.getButtonForSelectingDirectoryForSaving() + + isOverwriteOutputFiles := false + checkboxOverwriteOutputFiles := widget.NewCheck("Разрешить перезаписать файл", func(b bool) { + isOverwriteOutputFiles = b + }) form.Items = []*widget.FormItem{ {Text: "Файл для ковертации:", Widget: fileVideoForConversion}, {Widget: fileVideoForConversionMessage}, + {Text: "Папка куда будет сохранятся:", Widget: buttonForSelectedDir}, + {Widget: buttonForSelectedDirMessage}, + {Widget: checkboxOverwriteOutputFiles}, } form.SubmitText = "Конвертировать" enableFormConversionStruct := enableFormConversionStruct{ fileVideoForConversion: fileVideoForConversion, + buttonForSelectedDir: buttonForSelectedDir, form: form, } form.OnSubmit = func() { + if len(*pathToSaveDirectory) == 0 { + showConversionMessage(conversionMessage, errors.New("Не выбрали папку для сохранения!")) + enableFormConversion(enableFormConversionStruct) + return + } + conversionMessage.Text = "" + fileVideoForConversion.Disable() + buttonForSelectedDir.Disable() form.Disable() socketPath, err := getSocketPath(fileInput, progress) @@ -73,8 +94,10 @@ func (v View) Main( } setting := HandleConvertSetting{ - VideoFileInput: fileInput, - SocketPath: socketPath, + VideoFileInput: fileInput, + DirectoryForSave: *pathToSaveDirectory, + SocketPath: socketPath, + OverwriteOutputFiles: isOverwriteOutputFiles, } err = runConvert(setting) if err != nil { @@ -125,6 +148,37 @@ func (v View) getButtonFileVideoForConversion(form *widget.Form, progress *widge return button, fileVideoForConversionMessage, fileInput } +func (v View) getButtonForSelectingDirectoryForSaving() (button *widget.Button, buttonMessage *canvas.Text, dirPath *string) { + buttonMessage = canvas.NewText("", color.RGBA{255, 0, 0, 255}) + buttonMessage.TextSize = 16 + buttonMessage.TextStyle = fyne.TextStyle{Bold: true} + + path := "" + dirPath = &path + + button = widget.NewButton("выбрать", func() { + fileDialog := dialog.NewFolderOpen( + func(r fyne.ListableURI, err error) { + if err != nil { + buttonMessage.Text = err.Error() + setStringErrorStyle(buttonMessage) + return + } + if r == nil { + return + } + + path = r.Path() + + buttonMessage.Text = r.Path() + setStringSuccessStyle(buttonMessage) + }, v.w) + fileDialog.Show() + }) + + return +} + func setStringErrorStyle(text *canvas.Text) { text.Color = color.RGBA{255, 0, 0, 255} text.Refresh() @@ -142,5 +196,6 @@ func showConversionMessage(conversionMessage *canvas.Text, err error) { func enableFormConversion(enableFormConversionStruct enableFormConversionStruct) { enableFormConversionStruct.fileVideoForConversion.Enable() + enableFormConversionStruct.buttonForSelectedDir.Enable() enableFormConversionStruct.form.Enable() } diff --git a/src/handler/convertor.go b/src/handler/convertor.go index cd13541..3c3a8e0 100644 --- a/src/handler/convertor.go +++ b/src/handler/convertor.go @@ -3,6 +3,7 @@ package handler import ( "errors" "ffmpegGui/convertor" + "ffmpegGui/helper" "ffmpegGui/setting" "fmt" "fyne.io/fyne/v2/widget" @@ -48,7 +49,7 @@ func (h ConvertorHandler) GetConvertor() { } func (h ConvertorHandler) getSockPath(file *convertor.File, progressbar *widget.ProgressBar) (string, error) { - totalDuration, err := h.getTotalDuration(file) + totalDuration, err := h.convertorService.GetTotalDuration(file) if err != nil { return "", err @@ -102,18 +103,21 @@ func (h ConvertorHandler) getSockPath(file *convertor.File, progressbar *widget. } func (h ConvertorHandler) runConvert(setting convertor.HandleConvertSetting) error { + return h.convertorService.RunConvert( convertor.ConvertSetting{ VideoFileInput: setting.VideoFileInput, - SocketPath: setting.SocketPath, + VideoFileOut: &convertor.File{ + Path: setting.DirectoryForSave + helper.PathSeparator() + setting.VideoFileInput.Name + ".mp4", + Name: setting.VideoFileInput.Name, + Ext: ".mp4", + }, + SocketPath: setting.SocketPath, + OverwriteOutputFiles: setting.OverwriteOutputFiles, }, ) } -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 @@ -121,7 +125,7 @@ func (h ConvertorHandler) checkingFFPathUtilities() bool { var pathsToFF []convertor.FFPathUtilities if runtime.GOOS == "windows" { - pathsToFF = []convertor.FFPathUtilities{{"ffmpeg/bin/ffmpeg.exe", "ffmpeg/bin/ffprobe.exe"}} + pathsToFF = []convertor.FFPathUtilities{{"ffmpeg\\bin\\ffmpeg.exe", "ffmpeg\\bin\\ffprobe.exe"}} } else { pathsToFF = []convertor.FFPathUtilities{{"ffmpeg/bin/ffmpeg", "ffmpeg/bin/ffprobe"}, {"ffmpeg", "ffprobe"}} } @@ -135,9 +139,9 @@ func (h ConvertorHandler) checkingFFPathUtilities() bool { continue } ffmpegEntity := setting.Setting{Code: "ffmpeg", Value: item.FFmpeg} - h.settingRepository.Create(ffmpegEntity) + _, _ = h.settingRepository.Create(ffmpegEntity) ffprobeEntity := setting.Setting{Code: "ffprobe", Value: item.FFprobe} - h.settingRepository.Create(ffprobeEntity) + _, _ = h.settingRepository.Create(ffprobeEntity) return true } @@ -147,18 +151,18 @@ func (h ConvertorHandler) checkingFFPathUtilities() bool { func (h ConvertorHandler) saveSettingFFPath(ffmpegPath string, ffprobePath string) error { ffmpegChecking, _ := h.convertorService.ChangeFFmpegPath(ffmpegPath) if ffmpegChecking == false { - return errors.New("Это не FFmpeg") + return errors.New("это не FFmpeg") } ffprobeChecking, _ := h.convertorService.ChangeFFprobePath(ffprobePath) if ffprobeChecking == false { - return errors.New("Это не FFprobe") + return errors.New("это не FFprobe") } ffmpegEntity := setting.Setting{Code: "ffmpeg", Value: ffmpegPath} - h.settingRepository.Create(ffmpegEntity) + _, _ = h.settingRepository.Create(ffmpegEntity) ffprobeEntity := setting.Setting{Code: "ffprobe", Value: ffprobePath} - h.settingRepository.Create(ffprobeEntity) + _, _ = h.settingRepository.Create(ffprobeEntity) h.GetConvertor() diff --git a/src/helper/helper.go b/src/helper/helper.go new file mode 100644 index 0000000..b98d666 --- /dev/null +++ b/src/helper/helper.go @@ -0,0 +1,10 @@ +package helper + +import "runtime" + +func PathSeparator() string { + if runtime.GOOS == "windows" { + return "\\" + } + return "/" +} diff --git a/src/main.go b/src/main.go index c74719d..d04a154 100644 --- a/src/main.go +++ b/src/main.go @@ -15,17 +15,17 @@ import ( "os" ) -const appVersion string = "0.1.0" +//const appVersion string = "0.1.0" func main() { a := app.New() w := a.NewWindow("GUI FFMpeg!") - w.Resize(fyne.Size{800, 600}) + w.Resize(fyne.Size{Width: 800, Height: 600}) errorView := myError.NewView(w) if canCreateFile("data/database") != true { - errorView.PanicError(errors.New("Не смогли создать файл 'database' в папке 'data'")) + errorView.PanicError(errors.New("не смогли создать файл 'database' в папке 'data'")) w.ShowAndRun() return } -- 2.45.2 From 77b847dde65166769bd06ac9a54a69c493166d2e Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sat, 20 Jan 2024 01:53:25 +0600 Subject: [PATCH 10/15] Fix error in OS windows. Changed the progressbar to the pipe protocol. --- src/convertor/service.go | 45 ++++-- src/convertor/view.go | 23 +-- src/handler/convertor.go | 146 ++++++++++-------- src/helper/prepare_background_command.go | 12 ++ .../prepare_background_command_windows.go | 13 ++ src/main.go | 3 +- 6 files changed, 153 insertions(+), 89 deletions(-) create mode 100644 src/helper/prepare_background_command.go create mode 100644 src/helper/prepare_background_command_windows.go diff --git a/src/convertor/service.go b/src/convertor/service.go index e1828a2..1fead4a 100644 --- a/src/convertor/service.go +++ b/src/convertor/service.go @@ -2,6 +2,8 @@ package convertor import ( "errors" + "ffmpegGui/helper" + "io" "os/exec" "regexp" "strconv" @@ -9,7 +11,7 @@ import ( ) type ServiceContract interface { - RunConvert(setting ConvertSetting) error + RunConvert(setting ConvertSetting, progress ProgressContract) error GetTotalDuration(file *File) (float64, error) GetFFmpegVesrion() (string, error) GetFFprobeVersion() (string, error) @@ -17,6 +19,11 @@ type ServiceContract interface { ChangeFFprobePath(path string) (bool, error) } +type ProgressContract interface { + GetProtocole() string + Run(stdOut io.ReadCloser, stdErr io.ReadCloser) error +} + type FFPathUtilities struct { FFmpeg string FFprobe string @@ -35,7 +42,6 @@ type File struct { type ConvertSetting struct { VideoFileInput *File VideoFileOut *File - SocketPath string OverwriteOutputFiles bool } @@ -49,20 +55,36 @@ func NewService(ffPathUtilities FFPathUtilities) *Service { } } -func (s Service) RunConvert(setting ConvertSetting) error { +func (s Service) RunConvert(setting ConvertSetting, progress ProgressContract) error { overwriteOutputFiles := "-n" if setting.OverwriteOutputFiles == true { overwriteOutputFiles = "-y" } - args := []string{overwriteOutputFiles, "-i", setting.VideoFileInput.Path, "-c:v", "libx264", "-progress", "unix://" + setting.SocketPath, setting.VideoFileOut.Path} + args := []string{overwriteOutputFiles, "-i", setting.VideoFileInput.Path, "-c:v", "libx264", "-progress", progress.GetProtocole(), setting.VideoFileOut.Path} cmd := exec.Command(s.ffPathUtilities.FFmpeg, args...) + helper.PrepareBackgroundCommand(cmd) - out, err := cmd.CombinedOutput() + stdOut, err := cmd.StdoutPipe() + if err != nil { + return err + } + stdErr, err := cmd.StderrPipe() + if err != nil { + return err + } + + err = cmd.Start() + if err != nil { + return err + } + + err = progress.Run(stdOut, stdErr) + if err != nil { + return err + } + + err = cmd.Wait() if err != nil { - errStringArr := regexp.MustCompile("\r?\n").Split(strings.TrimSpace(string(out)), -1) - if len(errStringArr) > 1 { - return errors.New(errStringArr[len(errStringArr)-1]) - } return err } @@ -72,6 +94,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.ffPathUtilities.FFprobe, args...) + helper.PrepareBackgroundCommand(cmd) out, err := cmd.CombinedOutput() if err != nil { errString := strings.TrimSpace(string(out)) @@ -85,6 +108,7 @@ func (s Service) GetTotalDuration(file *File) (duration float64, err error) { func (s Service) GetFFmpegVesrion() (string, error) { cmd := exec.Command(s.ffPathUtilities.FFmpeg, "-version") + helper.PrepareBackgroundCommand(cmd) out, err := cmd.CombinedOutput() if err != nil { return "", err @@ -95,6 +119,7 @@ func (s Service) GetFFmpegVesrion() (string, error) { func (s Service) GetFFprobeVersion() (string, error) { cmd := exec.Command(s.ffPathUtilities.FFprobe, "-version") + helper.PrepareBackgroundCommand(cmd) out, err := cmd.CombinedOutput() if err != nil { return "", err @@ -105,6 +130,7 @@ func (s Service) GetFFprobeVersion() (string, error) { func (s Service) ChangeFFmpegPath(path string) (bool, error) { cmd := exec.Command(path, "-version") + helper.PrepareBackgroundCommand(cmd) out, err := cmd.CombinedOutput() if err != nil { return false, err @@ -118,6 +144,7 @@ func (s Service) ChangeFFmpegPath(path string) (bool, error) { func (s Service) ChangeFFprobePath(path string) (bool, error) { cmd := exec.Command(path, "-version") + helper.PrepareBackgroundCommand(cmd) out, err := cmd.CombinedOutput() if err != nil { return false, err diff --git a/src/convertor/view.go b/src/convertor/view.go index e5325dd..0ab80cc 100644 --- a/src/convertor/view.go +++ b/src/convertor/view.go @@ -12,8 +12,7 @@ import ( type ViewContract interface { Main( - runConvert func(setting HandleConvertSetting) error, - getSocketPath func(*File, *widget.ProgressBar) (string, error), + runConvert func(setting HandleConvertSetting, progressbar *widget.ProgressBar) error, ) } @@ -24,7 +23,6 @@ type View struct { type HandleConvertSetting struct { VideoFileInput *File DirectoryForSave string - SocketPath string OverwriteOutputFiles bool } @@ -39,8 +37,7 @@ func NewView(w fyne.Window) *View { } func (v View) Main( - runConvert func(setting HandleConvertSetting) error, - getSocketPath func(*File, *widget.ProgressBar) (string, error), + runConvert func(setting HandleConvertSetting, progressbar *widget.ProgressBar) error, ) { form := &widget.Form{} @@ -61,7 +58,7 @@ func (v View) Main( form.Items = []*widget.FormItem{ {Text: "Файл для ковертации:", Widget: fileVideoForConversion}, {Widget: fileVideoForConversionMessage}, - {Text: "Папка куда будет сохранятся:", Widget: buttonForSelectedDir}, + {Text: "Папка куда будет сохраняться:", Widget: buttonForSelectedDir}, {Widget: buttonForSelectedDirMessage}, {Widget: checkboxOverwriteOutputFiles}, } @@ -85,21 +82,12 @@ func (v View) Main( buttonForSelectedDir.Disable() form.Disable() - socketPath, err := getSocketPath(fileInput, progress) - - if err != nil { - showConversionMessage(conversionMessage, err) - enableFormConversion(enableFormConversionStruct) - return - } - setting := HandleConvertSetting{ VideoFileInput: fileInput, DirectoryForSave: *pathToSaveDirectory, - SocketPath: socketPath, OverwriteOutputFiles: isOverwriteOutputFiles, } - err = runConvert(setting) + err := runConvert(setting, progress) if err != nil { showConversionMessage(conversionMessage, err) enableFormConversion(enableFormConversionStruct) @@ -108,8 +96,9 @@ func (v View) Main( enableFormConversion(enableFormConversionStruct) } - v.w.SetContent(widget.NewCard("Конвертор видео файлов", "", container.NewVBox(form, conversionMessage, progress))) + v.w.SetContent(widget.NewCard("Конвертор видео файлов в mp4", "", container.NewVBox(form, conversionMessage, progress))) form.Disable() + progress.Hide() } func (v View) getButtonFileVideoForConversion(form *widget.Form, progress *widget.ProgressBar, conversionMessage *canvas.Text) (*widget.Button, *canvas.Text, *File) { diff --git a/src/handler/convertor.go b/src/handler/convertor.go index 3c3a8e0..88df064 100644 --- a/src/handler/convertor.go +++ b/src/handler/convertor.go @@ -1,22 +1,17 @@ package handler import ( + "bufio" "errors" "ffmpegGui/convertor" "ffmpegGui/helper" "ffmpegGui/setting" - "fmt" "fyne.io/fyne/v2/widget" - "log" - "math/rand" - "net" - "os" - "path" + "io" "regexp" "runtime" "strconv" "strings" - "time" ) type ConvertorHandler struct { @@ -42,67 +37,18 @@ func NewConvertorHandler( func (h ConvertorHandler) GetConvertor() { if h.checkingFFPathUtilities() == true { - h.convertorView.Main(h.runConvert, h.getSockPath) + h.convertorView.Main(h.runConvert) return } h.settingView.SelectFFPath(h.saveSettingFFPath) } -func (h ConvertorHandler) getSockPath(file *convertor.File, progressbar *widget.ProgressBar) (string, error) { - totalDuration, err := h.convertorService.GetTotalDuration(file) - +func (h ConvertorHandler) runConvert(setting convertor.HandleConvertSetting, progressbar *widget.ProgressBar) error { + totalDuration, err := h.convertorService.GetTotalDuration(setting.VideoFileInput) if err != nil { - return "", err + return err } - progressbar.Value = 0 - progressbar.Max = totalDuration - progressbar.Show() - progressbar.Refresh() - - rand.Seed(time.Now().Unix()) - sockFileName := path.Join(os.TempDir(), fmt.Sprintf("%d_sock", rand.Int())) - l, err := net.Listen("unix", sockFileName) - if err != nil { - return "", err - } - - go func() { - re := regexp.MustCompile(`frame=(\d+)`) - fd, err := l.Accept() - if err != nil { - log.Fatal("accept error:", err) - } - buf := make([]byte, 16) - data := "" - progress := 0.0 - for { - _, err := fd.Read(buf) - if err != nil { - return - } - data += string(buf) - a := re.FindAllStringSubmatch(data, -1) - if len(a) > 0 && len(a[len(a)-1]) > 0 { - c, err := strconv.Atoi(a[len(a)-1][len(a[len(a)-1])-1]) - if err != nil { - return - } - progress = float64(c) - } - if strings.Contains(data, "progress=end") { - progress = totalDuration - } - if progressbar.Value != progress { - progressbar.Value = progress - progressbar.Refresh() - } - } - }() - - return sockFileName, nil -} - -func (h ConvertorHandler) runConvert(setting convertor.HandleConvertSetting) error { + progress := NewProgress(totalDuration, progressbar) return h.convertorService.RunConvert( convertor.ConvertSetting{ @@ -112,9 +58,9 @@ func (h ConvertorHandler) runConvert(setting convertor.HandleConvertSetting) err Name: setting.VideoFileInput.Name, Ext: ".mp4", }, - SocketPath: setting.SocketPath, OverwriteOutputFiles: setting.OverwriteOutputFiles, }, + progress, ) } @@ -182,3 +128,79 @@ func (h ConvertorHandler) checkingFFPath() bool { return true } + +type progress struct { + totalDuration float64 + progressbar *widget.ProgressBar + protocol string +} + +func NewProgress(totalDuration float64, progressbar *widget.ProgressBar) progress { + return progress{ + totalDuration: totalDuration, + progressbar: progressbar, + protocol: "pipe:", + } +} + +func (p progress) GetProtocole() string { + return p.protocol +} + +func (p progress) Run(stdOut io.ReadCloser, stdErr io.ReadCloser) error { + isProcessCompleted := false + var errorText string + + p.progressbar.Value = 0 + p.progressbar.Max = p.totalDuration + p.progressbar.Show() + p.progressbar.Refresh() + + progress := 0.0 + + go func() { + scannerErr := bufio.NewScanner(stdErr) + for scannerErr.Scan() { + errorText = scannerErr.Text() + } + if err := scannerErr.Err(); err != nil { + errorText = err.Error() + } + }() + + scannerOut := bufio.NewScanner(stdOut) + for scannerOut.Scan() { + if isProcessCompleted != true { + isProcessCompleted = true + } + data := scannerOut.Text() + re := regexp.MustCompile(`frame=(\d+)`) + a := re.FindAllStringSubmatch(data, -1) + + if len(a) > 0 && len(a[len(a)-1]) > 0 { + c, err := strconv.Atoi(a[len(a)-1][len(a[len(a)-1])-1]) + if err != nil { + continue + } + progress = float64(c) + } + if strings.Contains(data, "progress=end") { + p.progressbar.Value = p.totalDuration + p.progressbar.Refresh() + isProcessCompleted = true + } + if p.progressbar.Value != progress { + p.progressbar.Value = progress + p.progressbar.Refresh() + } + } + + if isProcessCompleted == false { + if len(errorText) == 0 { + errorText = "не смогли отконвертировать видео" + } + return errors.New(errorText) + } + + return nil +} diff --git a/src/helper/prepare_background_command.go b/src/helper/prepare_background_command.go new file mode 100644 index 0000000..f8aab96 --- /dev/null +++ b/src/helper/prepare_background_command.go @@ -0,0 +1,12 @@ +//go:build !windows +// +build !windows + +package helper + +import ( + "os/exec" +) + +func PrepareBackgroundCommand(cmd *exec.Cmd) { + +} diff --git a/src/helper/prepare_background_command_windows.go b/src/helper/prepare_background_command_windows.go new file mode 100644 index 0000000..3e3ebbc --- /dev/null +++ b/src/helper/prepare_background_command_windows.go @@ -0,0 +1,13 @@ +//go:build windows +// +build windows + +package helper + +import ( + "os/exec" + "syscall" +) + +func PrepareBackgroundCommand(cmd *exec.Cmd) { + cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} +} diff --git a/src/main.go b/src/main.go index d04a154..e4955c5 100644 --- a/src/main.go +++ b/src/main.go @@ -15,12 +15,13 @@ import ( "os" ) -//const appVersion string = "0.1.0" +//const appVersion string = "0.1.1" func main() { a := app.New() w := a.NewWindow("GUI FFMpeg!") w.Resize(fyne.Size{Width: 800, Height: 600}) + w.CenterOnScreen() errorView := myError.NewView(w) -- 2.45.2 From 4fa977347c69006d525c956dc6516eed05ee009e Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sat, 20 Jan 2024 02:24:14 +0600 Subject: [PATCH 11/15] Fix ffmpeg processes not terminating. --- src/convertor/service.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/convertor/service.go b/src/convertor/service.go index 1fead4a..bb313fa 100644 --- a/src/convertor/service.go +++ b/src/convertor/service.go @@ -78,12 +78,12 @@ func (s Service) RunConvert(setting ConvertSetting, progress ProgressContract) e return err } - err = progress.Run(stdOut, stdErr) - if err != nil { - return err - } + errProgress := progress.Run(stdOut, stdErr) err = cmd.Wait() + if errProgress != nil { + return errProgress + } if err != nil { return err } -- 2.45.2 From 2596e822bdd4987fb8677f8e0c0ffc7100c0bcb6 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sat, 20 Jan 2024 14:57:22 +0600 Subject: [PATCH 12/15] Fix progress bar not always shown during conversion. --- src/convertor/view.go | 1 - 1 file changed, 1 deletion(-) diff --git a/src/convertor/view.go b/src/convertor/view.go index 0ab80cc..f2dc364 100644 --- a/src/convertor/view.go +++ b/src/convertor/view.go @@ -98,7 +98,6 @@ func (v View) Main( v.w.SetContent(widget.NewCard("Конвертор видео файлов в mp4", "", container.NewVBox(form, conversionMessage, progress))) form.Disable() - progress.Hide() } func (v View) getButtonFileVideoForConversion(form *widget.Form, progress *widget.ProgressBar, conversionMessage *canvas.Text) (*widget.Button, *canvas.Text, *File) { -- 2.45.2 From fc38a1b20c5ece8f54c2e9da278c75182a868e6b Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sat, 20 Jan 2024 14:58:16 +0600 Subject: [PATCH 13/15] Fix after closing the program so that ffmpeg would also stop if it was running. --- src/convertor/service.go | 20 ++++++++++++++++++-- src/main.go | 11 +++++++++-- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/convertor/service.go b/src/convertor/service.go index bb313fa..56e1b8d 100644 --- a/src/convertor/service.go +++ b/src/convertor/service.go @@ -17,6 +17,7 @@ type ServiceContract interface { GetFFprobeVersion() (string, error) ChangeFFmpegPath(path string) (bool, error) ChangeFFprobePath(path string) (bool, error) + GetRunningProcesses() map[int]*exec.Cmd } type ProgressContract interface { @@ -29,8 +30,14 @@ type FFPathUtilities struct { FFprobe string } +type runningProcesses struct { + items map[int]*exec.Cmd + numberOfStarts int +} + type Service struct { - ffPathUtilities *FFPathUtilities + ffPathUtilities *FFPathUtilities + runningProcesses runningProcesses } type File struct { @@ -51,7 +58,8 @@ type ConvertData struct { func NewService(ffPathUtilities FFPathUtilities) *Service { return &Service{ - ffPathUtilities: &ffPathUtilities, + ffPathUtilities: &ffPathUtilities, + runningProcesses: runningProcesses{items: map[int]*exec.Cmd{}, numberOfStarts: 0}, } } @@ -77,10 +85,14 @@ func (s Service) RunConvert(setting ConvertSetting, progress ProgressContract) e if err != nil { return err } + index := s.runningProcesses.numberOfStarts + s.runningProcesses.numberOfStarts++ + s.runningProcesses.items[index] = cmd errProgress := progress.Run(stdOut, stdErr) err = cmd.Wait() + delete(s.runningProcesses.items, index) if errProgress != nil { return errProgress } @@ -155,3 +167,7 @@ func (s Service) ChangeFFprobePath(path string) (bool, error) { s.ffPathUtilities.FFprobe = path return true, nil } + +func (s Service) GetRunningProcesses() map[int]*exec.Cmd { + return s.runningProcesses.items +} diff --git a/src/main.go b/src/main.go index e4955c5..46aec63 100644 --- a/src/main.go +++ b/src/main.go @@ -38,7 +38,7 @@ func main() { return } - defer appClose(db) + defer appCloseWithDb(db) err = migration.Run(db) if err != nil { @@ -66,6 +66,7 @@ func main() { convertorView := convertor.NewView(w) settingView := setting.NewView(w) convertorService := convertor.NewService(ffPathUtilities) + defer appCloseWithConvert(convertorService) mainHandler := handler.NewConvertorHandler(convertorService, convertorView, settingView, settingRepository) mainHandler.GetConvertor() @@ -73,13 +74,19 @@ func main() { w.ShowAndRun() } -func appClose(db *gorm.DB) { +func appCloseWithDb(db *gorm.DB) { sqlDB, err := db.DB() if err == nil { _ = sqlDB.Close() } } +func appCloseWithConvert(convertorService convertor.ServiceContract) { + for _, cmd := range convertorService.GetRunningProcesses() { + _ = cmd.Process.Kill() + } +} + func canCreateFile(path string) bool { file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0666) if err != nil { -- 2.45.2 From 9bb19a02630a8fec0a4fec6431a165088162ae86 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sat, 20 Jan 2024 17:05:16 +0600 Subject: [PATCH 14/15] Sometimes the progressbar is not displayed, so I decided not to hide it. --- src/convertor/view.go | 3 ++- src/handler/convertor.go | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/convertor/view.go b/src/convertor/view.go index f2dc364..db26786 100644 --- a/src/convertor/view.go +++ b/src/convertor/view.go @@ -127,7 +127,8 @@ func (v View) getButtonFileVideoForConversion(form *widget.Form, progress *widge setStringSuccessStyle(fileVideoForConversionMessage) form.Enable() - progress.Hide() + progress.Value = 0 + progress.Refresh() conversionMessage.Text = "" }, v.w) fileDialog.Show() diff --git a/src/handler/convertor.go b/src/handler/convertor.go index 88df064..8d3d5f5 100644 --- a/src/handler/convertor.go +++ b/src/handler/convertor.go @@ -153,7 +153,6 @@ func (p progress) Run(stdOut io.ReadCloser, stdErr io.ReadCloser) error { p.progressbar.Value = 0 p.progressbar.Max = p.totalDuration - p.progressbar.Show() p.progressbar.Refresh() progress := 0.0 -- 2.45.2 From 7cc22e1553f87e8198ba1473ff9a93cf41d0c339 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sat, 20 Jan 2024 17:39:18 +0600 Subject: [PATCH 15/15] Edit Reademe.md. Added a screenshot from the program. --- README.md | 4 +++- images/screenshot-ffmpeg-gui.png | Bin 0 -> 39592 bytes 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100755 images/screenshot-ffmpeg-gui.png diff --git a/README.md b/README.md index 2a30b53..6672969 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ # ffmpeg-gui -Простенький интерфейс к программе ffmpeg. \ No newline at end of file +Простенький интерфейс к программе ffmpeg. + + \ No newline at end of file diff --git a/images/screenshot-ffmpeg-gui.png b/images/screenshot-ffmpeg-gui.png new file mode 100755 index 0000000000000000000000000000000000000000..8ff24f6318c8d51f4e724ccdd1dbdcee48f97885 GIT binary patch literal 39592 zcma&NcQjmK*Z&8`(Sm4ECc0>&_bv=k z$LKTo9rtsu^}g#}zqQ`yk1RQ7&bj)vuf4yYeZIU^Q@lq)Pjc(lt$RwZUuoRBbzA7x zEqo86JHVYBDhf~F51xyL;)`1qLkwHM1%Z{Es@$zxRk5TvGeY2+*y**N%dK1F9XJ2* zdK`-_Zrw_kQFGkCer&`^>pv)RMvEqZ!QKG!l8`3D}B@aSt97 zfk4b4jRKXFMW21PU}{Oc7Zmv~`WF0K_cx5#GZy+6*73>C{Y+h&YHBnW2liT*aEm?$ zVy>IR-3M$3BZ^ERUK?o~^JhofD+w3IEaM(Fza}Qc7XFLAWQ^BXKKcebIlU1g4Gn%T%Nn>Lp-!WL~iC&hf?F9r|l)I%?*L;@RG<#;VCXKYLTytP`Q8O?(;m2}hh+!XJTReUXTOSbSD=~K~ zW3?ysUy~gBHOsQ=Xw>^pp}3l%uyYp)=bFT8(1G!m7%Ib5!N{Me2$!y`-hIo{jFdm+ z*1ky*nvp8sp|hiOckuFDYoD6(*vkHm!4y5k1tIXpJAHi2x`)tM zat6Cv#;E?5r?9pD)lWB3drI?jXaq{VwM66*+|GMfSQYyo-U502xaQQ<9kS<-5zLqz z`R#%>#tH3$8xktNc|M^+*4RWK#Y7LTkjZIFpf0a!19YQxe>v$skB8~U<>wCvGM(Cq z&ne@dDa9YH*B-Fn4fRNw5fh$tBR)QrmVOH7Rsw!9ad%@8elvJwPj*cDHvxF`*;YTV zs7$2l-@?M~+8t zzWJ(k`>^QyoL$Y}q0g?%oPGWJ=}n;}m((*yIR|a=O`+Wvyt#feEJ25VYGQJ_e#-;J zjP8u{yD5MCxSRr&yib>@H@Ny5xT~Q;U#NC3T2FS2r7dcP7AVDfGT)>FKC{~&YVW;M z)pMvQwAx6%(?5(5YP$o?_(?%8s$k7$96{sLz*|w}o!E?FQ$BO8q26G2ca@@b|f9VeA zYoZzA&gRp8*xe5IIeQu=Op3uacA)u@+N|wITG*v;u0&z4oy)%xvkcr7%KBjWK5t{i z)AYubKBrl{yTP{)!)_)Bszo_M1!&g5V5zAtk+ zt88D+?V(ABS+-3IbaZ?P+fRku8El`V;8@@H$&tetmN-5?q4AsL&3Y~>eP4kSZu zZzhT+k9K&VLKaG|uP+jOF=gz-10M!A?YH`fpG<$iDBAyABr!|FIcP3D0^u0njUXUo zM(l)CZf5!(srqA8s?+GtW9>=S^ljSH5_USh^df(fo!=0lhP8IPC|}b4n)(Tr?qYYD z3Uhh`3*55ENiT#a@Ei+6)k!RwfpLq8`{yInR00#X(=2>LtXZUX?>LfgRti23#_rUp z`&BaT!6zZmt(W`#AncUK+k}cOYw2L41&(w7#XaSIPP*Xr=?H+17C7K`?AuN29<)2h=zqx zPDwH2zKeIxx=xY?!52!ceLk3^3SZ2Mu8dEILDOI+)do9>395*z&rY*-s$(Z4Qc{xX zK-a$8R47h1_|la(Zlntl2r7|$OfBjd9xBUa!Oq?P7y6{yXN{Nxo&wP{+72_Qs4&XQ z^5u+El3*`8SUu`cs#n6;3oY@ zPhVJ9k(PEd1-^9L&1k=stSf`iUqcIsX}nuEcT0H8DQk8deHUoXrG#!a+c-I3{$2v+ z@6LZNmGi4uNG%G3Lpc-3pf@ihDK2%7n0*O-U{(@J^WribeXa%ERl10u^B!btCqzv1 z8eWt#_71%1?{CR@jvhS5?vlP-BhFXD=ix7pePgLqIuRSK%_W5Y)s5AS2SaLCR zfM>Oo|J(0BrV7Bj8k}?kH)Vj>g6MAQEcjo)d=cS|?xN`sLP}FQGQkeyz-}j!Tlop? z`+lcQo<1itR+y48+#1vMN53n;KN&1ij(=9G9!4OfmAL4I%JRe>bWi|KWJtQ%V5H~4 z7n^xFI;EP9KOArdziaGL&)jVa>96WpEjNaOw$rKU`U;CFQ8o5Go^aZ4ITeLKu5lO? zacPjz%|%OzN_7*G-$FTU%!<5LXvlU@YAG zUkT8;CQ>(TtTCMHaxi=Jx-z^I6WD=X{j7rnU*T}R~4f%%d&zFBH5Gr=1c z*Faki60G5X)$ zY9ybri{Hfx42SMH&%5t^{7>X1n>FqXJytuy%Nxlz1FYh|v4uTUe-~SP!g!5-{ngYz z*~oD0o=fD^e1Y)0Y#&wOWAr}SjDtwYoWyO28cM*evb|P+IK3()dqwz_(?hOX7?TvHxe?C``tPjd|sfn=vwm<9WkDc29+gZHy zn6)n2h*Gj0Mg;8UaZ9=VdxWH{tD5!s=2`Li)V$td;ZSjmp8~^P+n4NO2i2FXXhuxC z3*Jt=iz41H(eH^nWvsUmM~Z`S_K|&pC0eCX&_d6lLL|kI7Z~E{h`w5JNnPiFO-9oS zJWb}>_v!mK>r-@=)>~JAK`(eLMWZCd5j6TAb3GAkI)={j4QH#77{##z->MAV(nG+8k9H_e;zM4`7XHm3xvs7UjyP@!E%TUv@l=hlhVi3q<9Ryw+dFimsX@#yy!!p zkHq>t3Tl|V@pKIA#E@Sa=Rfs#k(@2J1p&6CVbo{J$fFCJge9trjH}M}zdDC0kJXJi z=B2+Rmw46;Se)~`f4$sxz#as8s+DXF?mAapuMUscuSZ<&{+Q4}QL8lrip zw`dx4ulA){?$Yjh6DC5{O>>i2&4*V9c@VEx-di=#f(?+^&w`@%TbL%Dwa_!}L zc|5gt=?~ad=Nv+?Z6uBTw9Zf)qsP2J^493vYe)G@TWxs>s(uz}c?g-`xdUf0r z>ztbCd-1uCNbZHeSG@jwjdxrww_f%U8fyvqzHfby{ zSG6plt{8QZch9h|BVIb|7At&Qk?fYQ%=(T~=VC0nv_sxdgcppY@_UJ%CiVWy%p#UA6iYi0$ZkaNXuA^O2Nvm+?i{RN~3u{o*EN<_(;8{q>6RXfj={(WQwr_(in#DMe76JDqoWvEr! zB4M)J#dNerdYXd3S(35)oz%d)p!bO}&gMMm@g4o zd{mQzN81;2>=M%CX*gC_N0_vswc*N8^i8MCsdgPCgC}bc zXjJ`39=nh5=cWi8NjCJ^tvzqKf7`gv`FuSkHppPO`8%!fJm9ebcO9D1-CNZ<`LEO9 zhkqHK-+8C?RFf$rSsHiHQS~C@?TnS@Q4j^#^A{%k_c7ofQ2=G>fD1T>*~Z>}x%Ii) zf-Xv0LFh#c#r|M$j+h8f79IYtWc&-|t(w~eu&oBCwafP^&YiCUUGFgGa6VF<6nC77 z4aPJV?9jByN%BcB758#jh|G@q^RVQ+>HljkTc!78_EQB7dZx|m)vv72zp70hZGW?- zr%CigQfF+B(i94E8NL!S=!;p7IYb%qeTN|_8OF${gdeco=gG#zKXdzeA;$`|JYC*=wk;vR+4&r*sEf1ssvxpPyQn z!2TpsarJ7=Mq0jDm$Ljm8?>g#md%AyvV^DUI{-)O7U)>3-Kvn5n}72~`N_4s1#5)f zeSFixg0&%MUI!QSX<|fAAai)@L;0kh!h#QR!%W}8mF-m^3V*PXSLDQ&M&}vo1oun0 zrtnSmitsK z0on7!hZ{cXS}{&9oG0>}6kqg0jB3QAyj)zH08Sk61}*c&9P*+sz@@IZVC{8bmH6`Q zN;avCX53-_c}t;72-H522c>TLgB?(ZhF8+)Kx@J@%Ed&(GmX?0Wb9D`nq}NUeB0(@ z4<+#@KCOh%beA@C85pF4FW33}zvzfHk|-7Mi(p^tERFTbJr_;O73`(jUoEeek?_6u zy=2m^kaj+Y=aK_N)t@pF36e_IZ0+i^R9Tm*u`duDo7@nyW3n8IwxdYn48#W+ZI{HApS?!sG%{J)?&l)!|N1I5{!DcH z9`eCh;`eLx>H{QI5QUbpq0JV4=fEPSi<@4Rc&v)O->}gb2sgsWDWI~H(dJ8d3clYb zo-|MIwNCOEba8Fe3NqA?CgFj)lc+w|Io#FZDpg8q{npo~WN^W*WutmdT~t5*rc|G> z;*){YzcZ)f4Cc}TR_ zNj2@ez3uwXx+f~U1Ye`=RPR*QiX({g4SaS$gLcKceWz9igF6_`B$oYx@qt)w5zKIB zyn!WxUZM7bw{4&s&{t?3fl$u=%PP)0s;Ut>h6(0fIg?a#=9Z-e5d^+GAM=cwspi6v zWS54t-%=mzs1uu?RYP4lX47dh1gH?hjo*~7!;&3pLygKOJM*d;W;MQ)ji(>-sPTzP zl)mcDA1!m>T95C`th+u^&9N)l9H*D(&3&r46lU(iUG4k^uE%_^nHQ^;VpbwDYFyBF z$?8<;-aHn4DkoU;L2g-(3~V4ttt60;<_8FLpbVZu%dhf0cbE+IiZBp7@|pM5Lgn%j z=xp_#nA3;1Ne?`szRx3O3e!ZCR6r1#?@5l9pXWxch%Z$}y_9;q11+i{uBleNE0f7? z4+a&-1pgQgKZ-eA#$DdTg+?`#lQx6*M{3NWaP_Ur-^+}q1CQH*Vll?Y*6vWk*ltf^x(;TQXLr@ytdMjygV0M%| z>)rTAlc8wxBiBAP9rhB}M;R1UTzZDq(EM+}uvL83KGBg~|EXsUHzC_7w$QpNB*9az zVuk%@d#$G;ca)vkHqE_Kwzw z$j#XJ^!k84i`um;hfGZ+wC-DnBsu-6YdrlL)vl`t1f1n1%-ZDN(;CaDM26 zl81i&!DVtUgfeUp;|uy-E9Gs&wLloUi82?>(LJx|r24!1-X%Tu5tnFEM{AvqWkI91 z3$vtC5xa`;7-tM@>fuH3qub8{izmOCBYT~RA-cK}&CV~@8yon#T(Vm4M1`j~#nAYh zQu8oI20vJi&_P|JWBuQFL`8_Fc==OhTn;z$8oe!U9)3&q+pB_y>TLBlK~&DvY)PBxY^d#MQgk=*S&0UTMT48ZRc{|+!F$eHr=1e?r8ZsR{kNyc z31z2(DsgTB6Za@wl4O<8C>>SeICXaG1K}lQoew49BgXKd8Lgj@RKCV9YqAn~%(A+e z1N%%Y?|uX=ca{F^GV`!L(QZ~&E%+MOqbs&8gUWO%w1u|$74SJ$Y+z=`k0OrWmax@y zf=7j;`#2fLtSL>(1_WAY-}3WO*LIyd~}+qVcZ}a^WT9*6M3~NS>}TlK8!P>uKKmDmw?QlgJrR zU%&_D-><@4CFK*mOqVz*hi;#QmEl`XJ=kf8pw&ekt-iVfAg3A#C|Lku~eu%dqpg zKBsLjm@2GHtyh663~FT%Dfc*l1to@P)z=q(G#C9Y7JS zx1<01>N0!|VE#Iw*N)9S%WJXY!}fdqK^K&0>GRV1Wg#n)wOajk*?WR5t5q$iBGWz! z_Ln#6zPz95SC(|h7XuAV?jzc2Nj{KLEcwF-r|eppIp_ZN#_Qh9n+pgv+N&+xrJ0XN z?fZiGbtJ;-9eoq$IIlV=RJJR11KM@6)wPt1my~6$Xab7Y zdKICQx&`YGH}4`JWsENUqgCASu7o${slt!9Wrm|=A@8Ci2;Ve0`~8fI;H)fqs9Uz9 z^HP&$BUi9%J5tArc;I8aVCnNE^RIHE;fFlh*)tS<_1Z4tKgzW>i;v#XfuQBZ;ph&S zeV0;tUnU7_+bfU|$UiDdtftWRNd7sw)xyKT`o2~J1`m+ambQd0VH^LL<}C%LlH26s zIgNp8X$R6&(7<;fhc46SW{~G=t5;4jPTUbX2x4#A-S4$T!{_Dc;|$zc>YN|(ljh7Z zJfJpUpIMOIdKkE}iv8LL!FEQUKE>0v#s2twnCqoirDx2Oc{ho!z+AgtWSV2>_=rwU z!YTXHKx7cz!HUlLV~wPbhe<;!_*3S3qcoMXAwg*@o~r#8=ShQBRwq~m6N0)(Uk=hf ziT9ncanyXwuj7xrR(-PP>?okWsoq??cqXhVe*QNhgR8^B<;+YpomDG_UST~QN{rL@ zs#+CkdJyb9k-^nNri$G*x%Li?0Jq(Nyy|sa(IV%(2P%*Hms~9|F)SP!;fy}k!av88 z2W2omXs%)+^Ln$Mr+;3}+vfpQ4QL4L57+AYv<%kqn;VsAX0sqI=GQc{$cTVE)4R8> z%|L>~v`;wQ;m8RzDX$)q7(6$edG(cRVuz+fP3MULzPq5aB9pJ{1g3T92R8(EAf1m7Y{T)+07+o)d(~OZhxq>iMESrYidx`b5PN zQ-7SalJRgD3wz@I@v3+$rTdSX zbAjkRcz@Dd--FiHfsZlCf{zTl_{dp|@3F=g_^#K7(OTp@?1`%W_Jzh$si0BhGPAcG zE!me4!99qRv~dLuaRkODqRm_@R{745WNDhl8G-#VMBfeCy_6(0Lo+qIcHaBEt4QQq z9}rl%EKZL5b8)%v4~LMf+^BD4tD~~q1R@OZ8DH@0_+WiB(XnPaOjG^bP*BKTZXMo7 zsq@3y(FBke?8E}_<_0G|f(j?_)0AH&nnJ9NJ8i9Wn1l2pOq#|0{;b; zn=+!=Mv}e36g(b$bAw83x>#{Bj1U!c0mFZ9ZI?e%|0BpNruz03_f0mWV1Xa9SH9G{g)|5F6xWc_q*D(%Yy2m75H z{pXR`3%r+;1N#6C0nm|n`VG1I|1h9;c?uy{msnKnGv6c;QPI#6&7x4H1om}J;87@| z!l)@!ElV=w*RNlIs0DDJ8xbGS)p&D#PWPbDd+I|S>RzO$r(cbo^}X0CG6Qe{y7HOt2pLpzn$Hg%d5{3d0S*lK zvE2_jT>KdZe$C)`V{2i}0I1n;0W9G;b2y79VExqF8=Thw5<%H+1^1<9SI^EwvM8Bk z{>ly8S@dlGgIcF3`hV~kPa}4(X)Rh1yBtKZ`tybxBrhpEwtwVke>`d67*dq&4e|>( z+L;k5tL_7tjmXms!jmM)sVJ4cTkcF(2{>(ymuPN|7O3D1OiY$=I`NVuZj0Z5g=H4T zNbNpjnKIns;}Mjm=LU2s%)qfl#4m@h@o)S}l8!MkK$FZ$3X(}GTQ6)<0n9ZGWLUK0 z-DS9s@(1MD4nQ2I%J}MRX&XRDO*H4wWUuScxW0-Z;J*W7ej$OROJ|x$nnLP#f4iINb$mB)WkYne`9b zw)UF#pZtdm!k)7Re!fX(f)41ii&p>{3--Dnb3@YmS34*mWGJ}5-zJbXg7I=16q)L@ zU!Ufy84z@y1#sd0Cw^xup75z^vY~Q-C$VRf-Vh8}%qJ8BzU9x(hbo=Ejew}X=4@yd zLbd8rZpqK;>*j=4rhyvDdO1~L^wP-42sf}MgIof*70Tt!OCdpg8so8YgZi@>Y3M*I zZNt&-oEX&l3}>mQBrzPrh{EzAi*dCz}+zQ1Y)8hO!fG4RD+{L|`@9y{$KRP==y-dql2I5eM zJ*$_NY05lQMhCgq0Q-W4mpveQIOHS280qs}9*FbyV zBoC)RnXlJ}bM0y6k4=<5ZPoe#Fy2t1A@Mwkgt3SCcstFQA+(4E1IQO(pXhx6_dGzA z*s&|JF&$BSQ%7d|?9nmR-+6EXxkAU%65((tNUY99@sEVK<$sR@y3(w&>FO4}w^NT< z5&~VjVz>#mrB{L}%}*lu8Xq;swS40~1;~$6=L5ygL%>N{&I^|fKoLFZ?fYDIpYGmC zia}p81E4#h(KqPqEY)lJYAqH5#$BBv9C=*=)Ai&+V=q;EBz7IwllokSBsiYH`!Hqz zEhk?(AM!9SRE%%h7fAqdW< ztLTes`}U=y?Jg4oU>K^zWeB~6X;j*%23x;YOAx2gSHI|CaCtM3df%|DW=Ik(5%_f2 ze@or}LX+&p*(jSQCp25P2Y}K~_KTn{7S<6qgp>?OKy@qfdi;?(+@^=Gq^`^rzWc$8 z;APOAdp6|XKmtsdAC5;^-zeTcsKlPsBNhr>)D7uRBD1Z#Z|m6Mvr(%)_+uw70TtRG3LZQZ1&y)ODX9RsYfKq?(W z0psJRr7uh(PnY~5YN(ePIU&|svT0#g`%Ht*8~YS#B^kYEAkoTWQd7m<9e;{Jt}1VS+4s{Im15sjLFqxCRX z#9?FvpF(n5wQTZJky>R;A^G?DURZhUq&}Kdl=BXiwUoZgM7-P8VG1G&+n)lt^wpqq z9*B&H9lNv{)7_Y{f-}~y-G!XVVQ66Mw$qN)Vs=uP0zocb&G?7l#}RM}&f?-uD<=3$ z!z1f0mmg=1dN}Fw0&&P(8%lJ>YO)QrPzE)aW@=^9tH6sEwh{u?hQ%9lnWN#e6p}rb zFouU*jatHgr1_&|p_{|G&QByiVWjE4+~A27gQW}YH%+X*Tlt71f0hGUz=>m!&GB?2 zg`I!>vP_!$DY0giM)$!b=1BnGF?cazW&Udrf}S2@>~w68dX~& z&;K1VbQJ}4o7A%s5!3wsYUv@kSWkhiTIT3{83b-qtGyCV+H{3qzz@3qBcvPH_{qcL z`uPyj3_d$mXq7Pg3b*wr=qF)5*0~#}r@q9RQniH0kEnYS`$9f@{r=Z?bVW=gbm1;Y z8L=Vw#m{F1l!=V2)g#iRoKo$@AH{cnj;*!|G6iw)Ps={QsxzeuGtpAnLr2jcGKgP1 zM+kq{MT4I8jyl5{3&2tY@y(%pIAKUnnxAZx^5nw}R;O?NS|Ud*7XW`3UI=%L-!Ozh z4kT3W2ieRd9>GY2zx!@bZXW1^>oqVHiEalqE-UO}c>{cD=*gQqI<#p7yD#*O`ckfUXS|)i ziEP2}3%lKsuh~p3s6L4ZtkL5amc4wC7qhMn{C@wdo}RFd#|mWD4yi5n$5cAP%I>ieh?giH+2emRq97f8}6z z`)svfpyW~hd5QF}U77Z>?kiIRE~j!sM81T9z}pp}xc$gSZ$Bf+vQa``_qhcnCIa!Z zSt;9HxFeAQ$sM?^!nS3BwmX;uNkM*CodD$Yyx+Y_&DdlfNRfFw?U0<1Y|zLUzrpP` zw^yUN=4NG-4aqE=Y#6ek3%FzQ5QBvHgDn*dYgcbWjJ6ypF1{SBkIzy#Us&=B(yLZ% zzSM99Jg}}Mgi7AMV0nh;JgVgYYfUg{{k5|q(J7F z%I0EAQ`&!W`qux~Iej-lsdW^i7;h8|Q-09hD|YX{DUtBSmX6QzG{m>>a@zoDAL$~R zK%RGH>$L-JF8;J9hM2EK3KWIg41z@nThU(66lI@|+KpOgv2b0xZ`*tHF{ueT%r^`d zsl#z``4VkK2^?Vt*F1t?xNePe`fv2FwbH;T zPNO_Ug`w9x$Uup+`2hWD9lAd;Z|>|NYin#VLn#!}ZRq z#nV-nl?8P$IYq1B(ILDipGCfH>&8 zyC2GDL+0eF^QUjctQ+Q7(40}{ArPLF{~(Qsv$X2 zo=jvCq3H)Ou@3mki952<`-JS$`=k#Zr(cwt%L2R1hXmK`HS!#7EwYyV+%!K2QVR>U zr9X|SiiNZV-3=SkTWBAM$`2>`C#HV&LHsa+wEdi7o{HD!=;{DD=SYOjbo&I87E*7% zQa&3y)(NiJZ<0CM9T952xbj)46f>f7{J^f(i?R`1&8km_uSKk<433u_qOqlXF>^Wt zqW3rLJyx08AEac_oMNgKnTSx9mY57KJ`zal!2qn{Q+nnu2Ns?3xRRu$$pgqf|76tc z--A&H;w6_^qxw$Np7%=HS%aqUn2r((=g2dNJ{VYAo!QI8?uGZ9hU^#;1aQVmR1l-u z9;CpTik1!s63kW_H~zrmW(!*@wpvC?uKGPz6hKmdDp7TgY(v4HEHYx8$zGkznARNB zlp_GqtuH-Sr+&|@hDv(~RkoYX!4&OZZ)APE(PytGb9NZ6%dTatV`BG5e0T8yGQ`Bt z+u_W!{Y;m=siMW~>|At-?5>}E|Bt@UT^(w+Wt1qcQXfU>$EWt_b84b(?pixt$1Te> z*J>(xN-&l?nWBz}kf%ir~tIJ9WLtEX02v{34NiI0e`H=|~mAam{Y++=ff z+SuMN3F&OYt;_1uP9{W+)h3Q@fcam4B^WE+e5i;ZntI}GF>Tp<0b~?27C3COG>K(9 z9p0E5g=l)Ji_?8LB>w2VJMNxN=r!0?-EPmV4KFODfF|=~v1m0Voi59@zjh_Gh>2p! zz$))YkGDzW*l%eA+enoZ)|w z!EXX@U8%97yKX!^akA3(x^luz57*e%|1i9kZy&NPXPVT7m5A>vt^uC~{SM+o+-HB2 z{)jrm<4RXz8rJoG6?3*FZ)Jt_rN6prx$Fp`SY+h&KMJsuhzy17H(S{)DiFF&_fD;f zBFCCMbY0K|9md;^4>yo?EG(E$V+R}^SC1VZkk49dmj%wzx=WwORuPZRw|IxuAg=TH zO|#Dr8?9bx{)~Lm$~O;BYTE|9O8V-lnsQ*$IaFW;w`~Ob(g(6n3y_%NOFZj}VHw@O0IE#s#UfYAHzRH(_l5%c*G&-Q2J<;H)|K zK8?of-rIJVH$)fm^>68=$y`o8u8KnKE^w43@%ZoC!BKzqI3SIikg6Su8N0b3`=J>j z+hH=)eo6LBgSYq3&i6AihU2oH7H=Caan2vsEQR{X_;hc4=ul{VqRrUwO5C%?eidfn zedXH|tv7t2#SOtX7i45ekR#<~;Fa7gat42$SeuUZJl$=WPpVly_#ItO7C9*94_4HK z2LZcYW?p7Bjn;K=smNrSoix*CB-Ld);V(cN`@HY3W~oA14|?tBry>i)7K5>m57z!G4S1{oAmq9=XbW~`Q&tGlBX=A9P_y>Q=Fr8g`ssP4vPUk z^^q25ChcyMaNWu~wd(hSNNpIcx?mlD%5OrZ=F#Uiy?`$0)1@Rf;y!+~o6S;Qq9k;P(v>u*Qc z#TS@zf>iaC=8U0xuDm((*c0J(mSInH%e!0y?X(Niom5>fl`Lq9@Sa!q(uWYt+739W zyT{mH;*NX@HONlgGNA8eWi##7el_O9!STi>MUCk<7H!-q1Zz_DR3#)-Su&|drDoj? zbaa;l2@P{)UYh&DDkkZZiN4+@}6=1`I-wV!IqWfDOn9j$Qoy?y< zv(RO~4=thH_uW|fQ1IpIu~zzK&Ch+kYoaidPii}*a&U%|?H|`y9LfHNW06Sm>xSKn zgMWATyO`G}C>HPeBk?0mtdic`|FN60i;3=lhbQ^eg?tV;1VQPD$a|$n_-vYzrozWW zjorI+6%wjHH9<=4I2df-dU1TkTkmacO=N5ezLj<>#Mc|U7TfNnXBmI_{Z)wDb&Wdl zDKx-vN-e7+F|=gn{MggIOr6Bh@5n4O%-%Hndfvyt^Ss4l7473Up0N^~Q8@(2Tx-&2 zAGEE(m~4ZZT~Chem;2F|&$Ks}E*h*Ql=_}Jz^yDn(L;R^8_8M?r^fzcTYx*sUD1zZ z#ZQZy0c#Y7QK!=%GFY}puP;w;^HS8yNO33kRJ;@oAKR32KR7S4w8X(YA#=ttkRIFE zG4%*+>M(Z1RzSWrEKxBSxx4>BVyo=HFn zY@6VI=JS*Kn?hZmLR-%D?J7>~PEuR7dMvT$O{IK)P&VGz{7V_ zR_FwNS6Ht8RT3aFU-I_Yegat&S3hT1!I)mPUbaso*}Hdt{reIIgf&UnBz5UWMMa;F z{)ibz$9k!amOYkow*z7xv35W?Ruih(nyXexuX+36n-^X3KH?+Jpr1l)hP|w5yc*qS zF(RTDLN-^a-|}giUhBSG4cs>D3Edv9Yh)#O;pCn{7B`gJ&CC(K|JzWHobui*z$$7Bty^_EoyYn{?3Cb=WV3lRAtKL~;&Qx*~49aK98To54A9UN!hcb% zIQ=~&=88b^+6UL>>7OUZ6)ffF=Q=P=bvNMkmG|0vfoa@H`gUk(&6@YZut|srwE^f( z5=zF&Dl;tNDV5L3apyuS)02w{czdPCv;?@v89I!g@3LuNTb2NS+0%KsXdYrc!yhOI z>NB)l@-CP-ERl~Epv~7n%>HKUpb)d~%YGEcBW)RbdriW7JvNrezc*v*f#>9RTmnn( z;bNZ3l&5imYOeIP``859tMALYk^l5Nbl|Ohl?%22%-WL2{G)xBp#%x@YuePnH+{A0 zGSu+WpL3FrLw#b!MHk%-?RUkaNy)8e_YsW-(vT}()F4`7!|Z}84EL0K+N0uZ9^%l% z0-lO@tZX!Hz=lJDu`$xTJ3nm32tS9JcnMsUhrU@;(i>iQD44!1UZcvTvnA}m$}`n{ zN}xyG7laH^O*Y`XgC?kL;2~`t^9dz;jv6v?pOFHyueB4V>wo;Dq`h|nzXls7FdDr|WMoR3+Y^=k`_lZeu-CHorHzQ&HDY$RuXIUk9n zbel$FzLNMCsI$KbN8R~o(m$rlc{s0U-b&cvURw45Nx1mI z`c+lh?vipOc@(R#F?u>kpfqDqoq^^ct;fg3@T9c?VnP!n{~7h2@E~4N`($g955Z-v z_6iZG_0Cj$z3!JNHqf!;ldfvjwTJ%c%G}QM{6;28cK|B$;fVz1uV)AYXNRmy`I52Q zyXKLB=PNdLBg^u4Hcr!zHfKc`5SZ*LM=~nTi}am?(>3$A3F&z6IDzZ}s$?FLv@r9K zNI$6@lb-rXT`}kb(84iJ$ZQ*N31;}Z+p%BS`pj5vaL$%cMV_3sf)xmz#%iDA9XopN}DAeuS2Pkb zNIS|>t}9yE(I)1#d=iN-&xgZ^ClRcl1v)+6&E6d**Xi5~kd9>cy=rin9 zq~>8;Li}FP`uN&E!+XO`fB#>V0ZK}AWr^~s>$Edk$%N{O5 zLV1fJStrCe{zOJOHKdf+evPP36Bxygw z`o3+$lk48{x#rKTo02mNk~Vl@xx25&{5etNo6?_|rE4@I(mGf2o=3spaMmQ>)ju_ePdR6ANLdrB67@k z*;2mTW{^HBg{X?XmMhq*l&Dt&)dygPOlqY?2ZcB+1gEV)F$C1Y9C`c1SwnGx?=q~g zFMsQFQaJ}|4VUq}7qprTX!P+rrtf+~7$o|r!1q>9{wF343=h787F4DiHv6cdDl6y9 zydL(+!;H@(3=fZPpQCogy0K&sHH=Kf-y)KaB4odR-y%NikJjl1Q~&9Tou}y!G!+u{ zlKQ8A!HCJ!meUvEU&;L?_@#h~= zG>k1luQ@IxElsPAR)>gNLst2|OwA7dOl1P1-ro+a*cdM`vmJ#%It7@u=Of#R#Y`yo zDHc|@^_bsYiupJU2|c#ulPH!5uhf$EARkPAug5M-@zNHV^?&$!>$s?*hHV!qK^mk} zkw#+ZE@==1q`NysKuTIdx*O>(DJ7&4h7hC?1cU);hR(Cb=lQ<(oZtD*`A>8hXYbjy z*1GQNR>E%h_KgI!;Kbz%?1My~bv_HFF8jjc&sIdW6jLI9R)t3WS?Ovq@d(Rc8`JfX)NI6OEmDTINcv?vW1Z~HE>TsJWa7Dn<^G5! zlehjj8-@_^MP>pwsxrc|^k8B=J2Mj?!Bj+ZXiQDdtkovtzZlE~pw@!Io*r zOFfgdXsED|2hDx($r7iZM1QNNS1@~AtQ>gJxM?BX=Ds0*N?w*yu0YP(0HLCX@L4+Q zH@q-=%pu$w;`y3?TaR5NmSsnF>hqeot8)>-ruIn0AR}AaRZ%K9)o@v3|$QKw~Xze;DAi?Q?YV;8sl1g9}+U__5re117n=TxqY{ ztPw$@p|07gY}U9zCRdQ@#g&=rpydyCNv29U&C<{x+ff^R%s%k*ZK4pL;K;R)Pl9Z$ zuU;)@&Wwnc?#>NIbL75WGQf=*VkbNylq@a@b>j+&x|4}vS6~c3zL?3};kOZp5)3bw zP^$JIY_HXriL>@ge#M%!(Kt>cP82^zooSjFhI){ZJgo%7MZupq_}DFT__AQ0HSgic zll4Y6RZi#YD`ig7&F$&r+jeK&sY_>VxvQHu3(g@afnPnBBb&=+IGvTQ{uTs2Bvyqm zp9HoY7WkU?ha@zVAx)lT4WrOAaVL`d^LExR#Jg^awS?I;C<&hn7&~7*;z%)GSkrX# z-3vKK%&1Xk?Pdp@SJy{k=rjj@UFR6p5ylg|P8M{>Pr!N_}V;g$JT>ln8nwC%)+ABTQCgv z(T==g-WMR=GO^B!Od)P1Zj5?P92iV2d^Gs-9-M%+eS{d?61_|A#93#jhGEv9OLdk5 znNsQf-<3@yDJl|o3PKh`kMzRaBR5r$Z#n1gudW<@CaR6j~@G@f;muKK_0dhEZQ$3xFX8&XCIT|qV>qGjBXB&A}l0m4zC3{Z59W(3p5nY z^7V^76WTUoA2<%z5YSDW=YMcFGdgLaQ_qi6p%^>+F>4$H}lIcp!i5vG|SqukPgI@3(>a^3hOzY7sepGzmKFiC$E?7>YF3jcih0 z6s+>(fQ71aRKP-A-X!yUASN<|&h<|G=&G_la3E~Hb8^=)Yu8UnMtau$AbNbjBUCBi zNFV`&QG!f_KULyZnOR`0X;#9IGZ*j8x7w7`b}-Gd?wi4md%&`s*3Rj@Lin5NqN&o~&4QE9&^OEKyJE6dSQABf+&*+y@m^B+w$t*LS6co5< zaZmZ2`Fr)zx&10m`zYh0Y@Y)-CRKr7kJ0gHsDy1++N`ag^>hB3I=eEX8vU?IX=qmT zRB6MaziKU;RcYHEhU}l!fMaJ_!~02%AwYh;?s-cb@t`ijch)f8J@btFh4uH_!k;wl zEOL0cpUCvwXkFngf8O>|MjsO~5wtj<9S~>5ehocr(lXuh9+_UxC{V;JOnOD|$^Auf z{MmjCZvXURvecaO24AJ|59^`79_JFg*7;>s>Z_lE<@&=bPx?2lwc?q1ka1YuqpgYe zdCr9d1Dh@Id2_zN@YZZyG$|=8^wi0%(>I^^Fho~=M`hL8pSQVURsT_P%>uBm7jw=cwPxZq zFJk<-jDgx2*>brh=8gPjlH~f&-BvA1(bb6g>o$GO=QLccvcok&x5_e*QYeQETWRdQ zpEwwCAM`sFv(OD@OD4g?cAsNt&5<<7p{iwzB#YdpT|kw4qf74$lR0Y(zCWGkUAEy> zwVcQPaFn&%VKj$G>o-mid z1s_O$u~o+zomd_nPR7DXmO8o>suZ-DM_<;-X37)D1lg|rE(`n-^^ugJ1@3&5jovz# zEUFNyC@Q3>K}+<0wbkuNo~IJWwA*DaK7%bloZ!MQ{<-h6@$|TWVxvxF054A-qar~H z3oDT&H!GWx7WQ}b&!xw58z0zyfo!yTqQ;Wv< zv`ur8L6amp>SM#8++RW1%xp9LZk02np~KXpi`G_oi_xCVjPVcP{T@YfbSPuAo8_hzY+2SAgCvGWE69i%F;*L15#6QrJ2@l)!_Ob-8q8{Fl#;~*|CYK*O(g%yBweI^r zDI#6!FE{g6;gOJ%DjdrqPR{>31={Hv!!8GhgwNK5uhgzydUW2e1Ca#66jH?bAk$@1 zt3g{+BvbQP^fvh!3!q5>nfgCok%A0{e%fn<*dsmpez*<6p(0}b1;GCET7L13t+gVL zEFF*A+6xr0I(gDocqb(mdZpltSnCvoK)@2DNzvJiJT^dCv&j*!kackJ@B2T?O1MEV z>TrP4B^02Bzy9+QpOft=CTuD}E`-D5zXb39r|Lvg8qiV3t-$=yYn5h1a(7xA7&b^7 zC<;s>&lEE~emZ>}-~Tw9JKmPzvZPMCE9uP(IpgtYF*NoJ29%H$FC$6EdT_z2WT+XRuEV(0_5HQ^%XX*G+DJX z-)Iw1tN4Fg!M(w*7_Wd_{)z36N&}eHrUBp+?_w4#CW{nT0VNn41O>T8eRuz%P;_jeiq{Cy#-L}a|CT@P4%Hq+rOxP7%MmSa%&0f`tM{NGL+u;oL`H!H;dbJy*Fo!j%SwRn(mkHU4cETJi1q#afD!_v zH5~l1Q$ra5L^Kh2;umxP;B2|9n0wx`L^zW3&8@P!IcjRM)AauKJZ>l7hr;Sr-6$B=ikM%56*V(B8-iN<0 zLP+P62umtZf(UiY@i2oWKbxQhdu&f1EWTv_(O7r}>eu~g(OEb~Anvo3Cg zaKKJ*x_69J@+6}^nakqQIoQJT|i1qxldMjXjd+W@4djPskjqsh^%+GN1(3dmZ2vP@ODF^Hf z`97_w)V)B7#`q$X1JD`0e>h9K8KxQki?s^jd@HOPWc55J$wfASal(0BiYgtoOGzUGss% ztB9!Tt!ahXeEyn>T=*h5aws_fsl2((o_DOA;xrs-$D)fI^k_=gm{o0<>&cuDkb+ns z%>o`$xBjk|_6|RUpgG5XH8nNoEbg-M5(H={P~yRXES^fbF~p6ucm+0I%=zJ>LK`7= zqZsM~IFv`taFE<4n*b(pzT~lP(cq_bg6TX7M}6ngajLzPDgi7M-07%rkkC&-0O64v zP|ki0YXDBW`O&b47zpz-zoe%VUi(1bgD`Wn(gK0QThTo4JHUYA5HY=Ukch3oWki)^M_^f06u61L=A3dZU2p9kK-^91rZt0Yn+Qm_#jeK+cMooAzM7aUdX zN93s-!G~#fhWX{;D}QRV3w zwy(Jy@uXc&ooO$7JLx}M`H`rlC1E<0dHHLcG^n5U{$kK|n58rnaiJO2i?`322jBT0 zU9($6LPf0{0IkGQ5NVO!IvL=0u*!S8V7DsJb*XwWm3Q~JG@(CzKH@3uZ&Wcqt^>d} z0tj3C1Mx1xw>S! z1*pp5Mh|fy>r7=;gA%swMWO=n)@LJ1mv*-sEY_at_#@_g@|0f4}#ed$4R0i?bd7Ji_ubZH+0lR^hJp@X~Y!h6sHD=kQUYT9_gsb?b4e~9( z9XCNt2_svx4d9H7iG$!~;s;oaIMHJTZ-gz&&|{RCC$asEGw@Owx`w~|3RBmix@j$- z;qGx6lVyLaG(r@xsrhU)auSUU8Pk~s<2SlEtxu;m5jS$g$-APNcO<^_0JJd$ar3XDpXJ$Gv`ZZp4@1>yw9~ z7R9JkKLLuHpoU}CILaR# zs96`^AlRQD6eWs|OGO3<-jFP|bG!Str+DJ6m5Pf>3>k~yiyqZ=dpJ+jNu?p1swDAX z4qW?5+2D5KsjBE7?4~u|`Y9$vA_t+TSO=38{jPe>ETVqIgpVAkcMrZig7F8e2dI&{ z;IaUgC6;}doU!1q6q&wCzs0%UYgxe_MX?K(BjVdE?1_8)`3Ozn5}!_ejD=ovZ!U?! zoW@z!k$#A#p4zg&-e@|TJd;MF29?$F!<{a*g~L%Q7< zvu#+UIMs$U^dAzsoc8C5(0N;-yAQbA4>0NjpOWS7AcRe0Gq_UTZ-UXVpwwL{iK^z< zVT!^)3I-Vup?y=7L{Qb#-{v@f! zU-}TO@2VywS&j{gOJ2>9@5BA+LJ~962}B7b)o1!4N>RjresW~K4@NTENYNR~v+MXQ zn#rK0WKz}N@O+swpu2dRN&sS!5;d5#9{1)n7ChvApBt^p#?-cbl$MFdNVN#=<yo!Uc3aV?v0Jj={yKLkSV zxK9)nyg96-5Yd*4mh_Z@d+aR>&F8j~3-`b?BDf!DpQ|;Zq5PcqezJ(&2RJ z|7kqMzk#AWV|upptm%UEP5i^r)ru#AN-1{Dk&M>cshQnP{r@=hNt5CWSw$;xt8fgu zIb$x`i%*s&f-h0={2oB+9>ig)uy(Z%Q546J_$x`)MZUSQvmX+FCEkSp44>V#@`d_P z{U6pRs)HK*<<-qA0oQX&3D#cG9A9j!Uc|d8N>My;Cos9F65!jRC$qR_4G});f?y;8 za2VcjVU!jerR2vC&w$FNq#|do$rkFbILP*2&f_BEXO1-B2IuOHHrGk`?bjQkP}q(2 z`sQhif9|~M2_~N$&7TLU7sDyhzuLY>-HKtp39=-S-7hE<*{dTqg<>jFuAraMbqdOc zJY=KpXv(+AG)O{!RO_B2B2T4Lc3HO%h+k(Cx+$9wzF8}o%xI2wGJnC5`MD|7+8LF? z($JPwJ}h(WTfRCk9_hiL}pZ zPW68)DhI0YGuT)UQ8VPJJ+&MwWUaDI<2au|s1b57cs@32RPr8To*`@4O~r@$0nVba zkOK0e8lCj-q~;{6Lfx_514J@{wgqibfiZzo1=&0kmxRaCsT&E~k$y017u#eDl@FE8 zubXI2ohF(1Hg$`$msp`8JsFd}FoC6_3S;776g)3s^;x8&_-I?}5&6LREAG}+crHxi zEDNR)-pXwC_3}o!Rt^3dov+`tOb~BJE$-5oK{+{fQM)~MZ?jR%>J|r#Hx@}rpz17t zP(BWmrf3ET6l>on&Y1h4(rV=+QV?@YHo!X-bIa1Yruwa~uE}m|ZN@aKwPO4sx6}*! zcc~3@`Iy>YG{zJRwQ8h(OL2Ysi03g;8{vrBE7zEVDgpLysy>BAnhH9-enQxj%HDT# z{KDI$wUb1WMT4li+auwxh6$0ynpv#kTd(q(6Q0zNiw2#D_2LlyQGcM85!?J*c?UdO z#ih7pP9;2lI`nigECg4W8Fxm~I_z5*b5=n{*LlZ5Khg)!kN}8Z(5nQSz?#mhzuOQp zURwGIi@>=Er{;M^$GUFxqc-q_A(qNB7<>oG#yhFBkNzx<+^erwJ@d58`lon-mmJ~KC3SJRRGym0d z1L^!;2TUyhb^m-()u6{dq6o(~#pmM2tA&Z{GOZ+R`!-Zx#HfuZkYApm%O!SF{4agz z_{HLrrUB1=1;4|Dh0RAJyEj~)f(N(Q49hEK0t-7@gJ1S1ZQ7Q@h4rwnV6MgO4Yc?< zx0uUAPvfm@5AHPUtpb9>l>Ayf!g5Otuvv0qS3%s3LL`evUvDFgg4n(RRjVO-FIx`} z?!Lj{Ptc_EmsDD_dHA%n1i7m8`GzDB;DR;BlD#N+^BbHlV4k_2kIzDmOGQ~nDoBRo zX9=*IURUfYuu639M3M4c>@q5>e!|TQ+-?+G^%1uj;)ma+BU9~@KE(TtOqy408X#^n zb#C|Q<;9)pB{VNJqhXE($!yU(cF!!Mz2w3ub!Obgr)gVp!Fkhyy77vdVS6NKJRD>S zF-X6V=SPnh%uFX+6N&47e+u1y#7OI2RZ5Rc&ic_($d*KwuGd{NFud9R;Eo7i3mszE z&om;V`JA=M($<=RY(qKN$-;>Gh(U_3`&ieRp)+Pk68hIia-$0p-~Hc>>J*wD2HR98 z78FxdOCm0E(XK2&L(2K5X#8vq?zdTVAdievQVQlDWY|M|E?@rE-n&k=;-MXHggzd6 zf32cCew5LF%-S_FdF7y5yW}hm2U7>&w4s0|6Ks11ID}fi@|#OMQkci)dPg2Yu2rcu zB@hf_EgXxLG$9Npv3`$qm9B-WraE|}AHrS)yOEB6Hr?aZ2jyre@~QC8m-GKav&%Ec z=QGXa*@DF>hD)|DWd?{s*Ei5~89;gna)8(H62|c0nk+rU ztk1Uko@|+ggJ}f+{jxxHyaMq_n*S9@A0MH8skuWcfQSj(y{#mU+XX{cvyta5F%mNl z*8jg!*1z&G;|B=RTO;yk1lN-;HpHJdUP&CUKK$R-*(WC>|4kSF#3O641_|V%W(P+{ z>~)E2-gdsLLawHQCwap3xxVK~alIo4v5yMHX>!z3fbU!QdE>%x*X^>v+y4`=y_ic) zQse*khwHURZ!gwCJMmwNogQTWtMIF<47>pEQ=+&~fUPk=XTQ3Top1NK!U6J0RNF;C z@>f7->qnD8S!HE3{8d5+pTYQgZ3iLA)*|Q0F8^K|XEbT!qw@~>T&x4jl!H#Och-w5 zd@u7vJ(avSs|9*S$3;{2R)|*b(sJ=?+wYyXv~*~%-X0JLd;NV3V*{hiI4lJ0En;08 zsw3geDSG@@`AGLP&tZR}$phwB6OYD`7sj;W&9DU4)>nVk=Y_tUZ*%)YOd39Q(8&-p z8hb6Qj57?&UIy4}IFWNFXZ<^1&gH2fjxz+Q!GBraoW_HaE2)m- z^2ANs@lG*_-wtiyqkFG@wAH2S1MD=hcYznPdjjDHdAH-PXSZIKDHr9J+0$aTn>(w0 z1~r&A*2MbANdoeLk6*T}msG92d+iM`#9N=f{)6D@N`GbKUYmY_bz)OA?6mWc{IapYF4-o8WoYb_$#OoH{^Mva_ucF$_u;Cy*RbmkeIf^EHX4x_c(z!e|O*8Ot%UIA@N!ZY1-hXMof+eQ}69*)u* zhM1ef+rx1&TxYXA zqsr&ame2sh%ZuLCO`BY7mw+3yEr1NObFUzqUB8~i-r?vl-1=4ROUC0S-MUu)uy=19 z7wf?4!Q72reEq3XWrUNqu^2Szb(Px{pz4Ru5%l!(NU9pp0W?>I2>fE?xBGep4v`#A z(WKWy5|N{esXJY^b8e?iy?pZNSDZbRX>2DUwG^(@5uF zxzVJ)($+hzCQg-A^&Ajjj#=TMR*k32yf=}qI?WxZ1Uojh_u9?N49BxcQt$5e1bR2( z#PDeXZ&SD!vTWwjNMm>AOeY={h+{Di`k&c(9|EqJI8NfR@%`@(GD|`~$L5}4v&!zu z@a0R8)!QEzFeD$j$gir>9*La|q-|%)_PEdCuDe{Uxej&C`5O9TeK1V95h8XPo7FRn zl8V6;=+fm#P1GxN{)>G1?xL`+dvYY=09%g}U6%L9^>{~1*h6C)f_HECNKj~pgEu2~ ztOie9zU~ptj$XniMmYP}XB}ueN#DKM&P@3}X;ypMN%9Xq#2Z;R6oz$v?YZTYpdGMY zY*X)Q>*b$6x(9F{zfI_X|CL?y*kW%7r#NNSsM|7r{C>j>`Vmz!kcn4&TY_{w7MQ=a z+Di40%gRW2yzhOt?OospFf8m5_RoiJp1;>Xqiqp|YiS0i*@TBF>V=85dG$5cCw~2d zj1?v|zq2^y5?bO^y5wWwee+SL;e7$Ob_M1;Ik1GNGR*q*4cGmz*QA5*|4y^p{#|Zz z@ZJdVve0beIq2j=m!l)|K50tpQ4}ztj2&l%om{7LZBKDAEG`Ewzx5ZS&pPk-T8<4u zs%<-+FEz(cB~(Op;mx2|ZaV%HZ9{y2*(+ipj$3OzQcR1Va(J3*+-yxZRm35BG~iHg zGU{s~fq8oBIe#oZe0CJ;c-s;>+mMfJ*@w~ovID+(cnw{6`#UmbaPMn$js&!n z)76lAi(Vd=P68MMSwXF49*tO|sdYU&*+tV=ib*{6N#1y*zZ+FhjLzlJ*k7u8>X+IibjpdH?-1)6 zR4E3mesm~55^wpJM6F5Be%Us%583_2#|zFLFHylYsA?U4)2(K}HTSLWds0vM{P8{f zR;I@(;+`Y>3!sqFnr84kP67E!ac8+ZC53-9YjlR2M(yW~2&aVgZ{|oC!hh1Qe#FZ{ z4*m1J%3REJ`Lai?^XQ5BVJiDS%}zJU(YsKIofva*_Z zY$edP^{%e42WNh7Fw2GXpVly-g0NaqL(LX~uV@qQ2q%h#pOuGM-g zVZQ#-PSu@V6 zQu6YHsy1isT={SW1>C&s-%G+)m00@`bS?U5WN%eV!^d z{Om7Lmb20Q&-Hp~weYYKSuH<_EKwm7eGE1Z2-l6}2zA3ZtHQb&LgffBPo4LA#&@8^ znN-xANY}T_X=7OT?PztS-zldsrW zEYxxAN#!zxkh2GuJ_$n#`|U8wj$y868NVeVGv9dU(+JZZ&8-aY)U0hkcNXP`TkWiP z?&0*b`(IfOT1sM55g`*m7+nHIg{@J9BFG31#$e$QZNBG^G$~33kP>3BtdysEr_tF7 zW(=(7$vzPLex8^@-92^7*@2%X#N`WpXUBR0KwI4Lb}@)!`)(8u>Z`<}TJ#WdJKWhy zF3aPs$aOf)Lbcb3M{iH5CSwF&nl9^^*^|Tc4ExI86*se~lvHfxG4Y;%HeeCAV^=o2 zzS2Bp^?5HbG)wtI2@X1`Uv}<&Ib5vdcr<7ug(`DnuA#lku%$3G_c$w<`}1KZr8C8h z$-B0>qTMml@Q4sjK`|<~`AfHC$i(2$V@UMX07yQ}OW0@?u@@rI3ah-QA`kQi`DxR*Q<7tf_bq3|Y&Mlg)4UwP>^6 z7RaYL*Vkf_QGC_(H-bB0E|ERWsOtYkpBuT+y*DYpAKi^g?6+y?Rc_V0yyHTF^18uf z(AZaR?tV8Ak%gj3lbLEvTc#v#6heJniJd-E8yYjms&}ZK7e;}qhN_}wDMcWoUJEg; zcU-J^SfknKM3v@5iBY6cH*`3ST8Ihv7d4Y^S6iXUjHt&OH1^VKy$-WkKWhv`W#Se2 zvy@rtrMbfFy37&(Jc}-&!O93|^Goa8oAKH=&tSQ+*Y70XTIkv;PU^({8)-$IBG^r~ z@RQ1X!a0%_>_CWiRg13K^mYT}SHo~%l!N$K@Uxl*rjuGl88JA;jA~=?%3iC3pnq*h zkAOBcA=^1Bhs}zwFZ5e{kZC6$sfq9nfW5;(Ds6q+nG#3e?aQe^^}8$oCzMhke7N7B zHs?C=s6JSYPEy{VbqDnoHt}daQuKI>q{j*^gwc`KmQk)h&uXvb$EH~=4*&N}r{Ul4 zO+#K@YZa01E^^-3U%Nzn4RBL&Buo>&8-G9QXiz`sL@!wwat*SGA~HTyO}Ab>QMV8K z9I-;kZfsU$dYm0x$R0lR-njKz`-Z_8^Ny_@ezbMyN>UUbrsrSJPSh; z{pr>cANq4tSIpAC>Fh%r?Q~n~oG9qoDSJB-0M?{(i}gHSln2l812?b1DM1J*0XJJS z)_g?XZOr%g*yg4_uiZQFh_PPRq4nsypyAD7M?ELH&fWHjs=Nbh;+rT=?a(ws)9!S= z{HIDuw3!eBX1B1Lt5VuHoi$9dtJ7U_wx$ECZqx_3{=5N=>l|jVCp&{t0=Gi8sZBqF zvHDezx?b_<#2MFrC#0Te!%4U4ocQY7LeeO`JHkXXFnDWfu2Z#_{w8 z+gT;9;ZqJCGZEWkewPFB)dC&4z7l-MRw;zm6*_|^h0impf|;${y0$tBpU*1RB&+otOA{7R{Q1$*n8g6&Bh zdBdL=iMNXIIz*@gOLB0~bhG!!!lERs`+ah{q=*@!<`{EI$Hizc3l5qS4*A z?+v(46XErDa=~pcF-IdKF%u6whS?Y7}oQIKwXI9@BAX0NnaN>aMo$ z)<5hi##^Bs`*MCZ7bI4>7*6Z_MYSq)GIZx$$A}P&TL%P(XTXy#Z5H`QkjvIC5+M8R zr~9^B*1Y(-32!+r#e%ipOH@Gg?yqRdE^!0h2h)(T->3$bSz8g(bjW9WW%suNvAAct zXZq;R>4;=t-U0O1xsIKSxxT{Mhh0qrU+U>ab*_?RH*=ZJ_VVF(bs+N%g3{{PyUcj3 ztGWVMiCeb!OJxZ`1+kZ_>Q^&M>1Zx35)z{PCiLrlZ@K?c7DoJ7`LJ?qM&bWv3)T~$ zVaOSXK<6zFhmjU+_Dh`=@AVuEGf4{Op+D8}T z-r>;4hVdHiQBk}~S%4cw#b1z1&5FT=c>8PbQvMtBH>9%IeV z$hVw4gimoYO$V?de=BQom7h0hZDmjM0o6bhz2dF!cu2$Wu5djb&aE_Athak_aym-^ zvU1+XpG?-ByCGp?i(q%LSE>tq?2|2SnFMKAP(yWeTD8`0TJ^^Lb>1E4n=caJNiy6? z$GD?SZOB}2BXjszj8mLczUFC}t_!#=1W-B=To+9&ZO@AAQh_`+g_*r}TPJ5_(Q@kR z`0wo4*CEun!R@mE z#JRWOce?ev-ete%>_2U41i4UQR2ejK+y-K7HsM1hE<`vB7*#Kq_O6ep1|aiS%R@7= zz|3O1+3c^qf5RUSPT0YJtXy#Nd81_T*cteriQka~hkn;A=}YJ=gSL3SDT^EU7%ini zEBy7`)?QybQU!(laWL_f)k!|>i#x*~p^8c7Q>?5jsQTN3A_f-7qYaGKwWD_y)J{H5 z(&XGdq+JQpQ!V0R@A|$Ke6aS0_O@FZFNj!rI@EDwHXmKHC+l@N`lCKkbTm(M!U|&Q zMp@K+?wUb87kH)MIgDI-(p%D0M;kN1Z+HC1+tAkvro<53U9X2EWZe^*9;qy)LNFLs zl3*FJdGKpC0s5@G2pBtnZpJS?*?$IZWQ~|YeYUM!%GVt;3I#COvy}hZ11EKzS3AvW zKc-?_JBd#Gm(b#3eg%7JB%Q|b>%V#l4EhlF%1VL%aAf=+-Tu7%FN{VY&s|xIl_Sgt z;4m=L$%+@i=KMFgykObd8vK|zT=axDd@M(#KY!pqZ*A9SZJi@*^+44h*kZ)=Zt13qcLGQ;3(r|fAgl=j}wDY;fz7up0ITV@A^Oj=Ej zj6ccuf4yFQpPr4{o`m8Xzmo`^1rE+$w0Yf6{EA3dT}gkbZosw zyNYKU0|5(W?ETVHAt4!{{RPSvIT0xusFo1$EJXTH#~gT z*udHIBTj+Gdg-M`=0wD%g}e}GYHKVjFtdM~MfI0*{Q#<Cu7w||=k4ULvc-;v=vRR*5K7xLDhh`Z?E?zT6#y6!A_wg}*#TGEK&nyn=LhKu z)_wa?n`_Us8YNWWk(uF>Z|T2S<@pzZ(#0Zd4*^VoYLrd_!~(P{s1(>7SMo?jlv=IF zig4WB0;l9ZA5iD@uLJN;iCj$y+H^TR$-6GUCS=~ah_3#DAD5Q=kBo9sXWoJKWgfTU z){q)4S!P^QvKve0k~-}&EvfVw0g-s+PL^6uiOnoNNnXpgct!?510!f}em_(tf&k#A zt0$e?q6d&8BNrE7y%ZCn-n#%b8|$xk5Vl4r*fg`@WR_Kcbm1V{bI2qD{L+C2?Vm@I z1TbS4Ux37xdF&o^tc6C<1_-|S`;*1-^A9;QMAOH32sZu*0$h{^Cd73(SdK9Ci?sqp z7ENXVz`zspJ&TzFipEawz@};K=(u?xr~Ja=w_~vK2!XKuQ%j2axc{SB;HJM2&dZBU zfi7}{29(QjuI?toj>N?8s1AYJRQ&`6O%wnmNtc*v0NC2on%YSUhbn?p<9FNMfe5-W z)xbG&XMeH9We!n$hUk*Y^99TM7MC~_yv$30yK~E9p&hR83|MI5F!lwjvZ09nH}S6T zj9Gm!2_Q#w1q-&Jh!7&!pONI&ZNNYHa4fsoFn;sHx|td$W^iyZ|pq;`En z#sbfX^l~vDMpV3^m+_n3bM=4@gT50K->GVRiO`cRd5wkq!@-C)pbc{*%?K^Ovp)MDAk9kwiAF);81Gi{L9XRL)P8kq{DLc_{l|hU1(Ir9-CqC#C2x!Yb z6)R~;MBQrGYi+&a%cVX*c0r8LJ%Cj7BPO2) zyY8cj9Yq20wu&G(TS4brFQW2|Z4RMo_69D)b+{k^+Ij(|M$Q(g71Q=~Y=_INCzOm` zefwn@8EFVXF!|<_I^;!7!#i-C5rs7J;$6!W-vf**dP**ihMKliiG^72tiB0RbF?Y> z^jDbzk!Xa6=S-xPPK5-%TPHS&!jagVVVR0iHkP;pT!qn|hHV$WfSBG9G_Dy!ZrW|ZKe&jBSmB4<1v zz^oS51$f`1B?hj2U@H|RaWq(QAC3GY()M!SvMS!!0E(C+YxBlS50cTN#ZEmEQ z(1h^eQ`O%m@($YCVtZ;fJnAoE0hFFX~0IRgk9 zDG+cP^tZZHaWK>)g34W?{oUB>E8z9Kt|!!MUl2d zvn4MPHLm%D>0*^ky>ls@6HViv>+J6OfOfV6)T;GEyQkD7awHZ*JxN-+(g`YF@S6j* zZKgBrJZbPR`Z!UDBk<0;k#FBTQI^&wj!&^Z`}hBaWv)djVuMK zgrOb+y`^FcUm|2J=^y4+_0+J+VwB>Ao6TF&wldGliQCm&kgvHTJx_>vZJ+rtm^%0y;LF9VRWWeC#}~~eIY|xgu>koT?l^FfuNS_08>7Qtt34VOjoXr|AI#t0>#Qg>}$pq~Fw)DV*%QKF$$r@4^>m3=2RPEc2L$C}L4 zO+f58Bi}_CFLnX^oD?XS#%gDvvqvq;_R14M$w3vl*-)C3Hgwzpgh*XDUbOhPbbdbwTt_l!eDD)t?*-{S8?`hSZSw6mcwDP|_| zTCZT?XflSeB#HB+CnUcP}EOHhdXvMWQ0*W{^fz6*v< z<92EMzgoWzMpO(f+qTJ36V@Hx=N9DKpbbb*Dl9L69HF27n#um-u`^h;i5_hXxZN=s z>}2m}yLRdpZxHLhU6*UD1+5dq0n&^y?yF6=SEte4So4{~R&_@CD-Rca0kk~0XQg0< zj3hkQ=!}en?nGx>{o2h1b0mM|v6K`yhO+H|ESEtfU7EE%f)HL8>MQe^4qi>g7p-^M zzIpnAnZHnZ$T8u&l7C)Qdfut~SVEYkFXn{Zm0X4vx;bhZuSlT_$xdx}%wnQ4M}`Dw zVZh64Nh;+%_25lUc>M0o_X9uNg$54R2E)~d#Ez`Pm{ZZj$}}Eq=(6CRXo!d{tovue zCoDg_>Xi%+?j%8_Vabx=>KihW6-{&YvrUXth|}Esv;~SdKeU*8RGyg{@g_WNRnUuX z{)DVXR1Z$znl?Lb&3>YI`hMO>YMw&O0Kg62Ix$k(_S zl(^$Qt2Z-6$}L@yuFs$d8bKp9#ILQHa?8QpDTj;c^nIsrmCd;mUed+8rH#S`AEFi4 z`NXpoT$r?2LQ9X7&9-(g)N|U^2v`8ih!N5O!|EiT)2N}m$nMVqu z6yqm{me)l^Ih+!}R6Y8Rn1>4WmF0MpjK{HGW<-f@HWO2}tMn3ob6S8F>ihQnIg7ZD zF!UTasHSuooPA6@n^I(l9rLX|e;0IsHV6tLXQY6pNNm7OrGsiOTseMv^%Cf|01CR| z@AkqNW+I5ZqlG?gAm$r%Y%;E2hs)bO!IrVKQug(icNn$M6Uz$d7hDOuZx~Cq`z~)nvXryIq`-ajTP1AMAqzQQ?hJhj1 zQgS29y=M2k=h&P2Hn#!|i-+wxw&$%YHXGZOqf50@7PTOr9;xb{y_xR{{jI*jM&?u9;gW0pY6)-NFT9r2R zH?r8$fFU6zJ|fu%zLFa$YkQ;FLp}eMIm0T|mR&xac?Y6!3uDVG0#6%}t1&J}x_u!h z_N~Cr`YnkU_Z*;N;kAj54Hvx~#nor_Of+ih`KJ+9^-vC{>$Vg^E)u?Y9YTgGTh(B7aLkuZu574r`-=@=TqhNe#Mcu+l81zc0VRgj7eN8$oh zMS~BOS9wgtf;O``KMqPb55Wf20vht`=Tj7v;2ov`iKEFSEs^Q$bWxzZxxp80Q)@*I zHjSF?-oLtBkBi6LPh2&zBE8ZELwPg}jeq`QLCW{m_=RFlY}FsFYG+|U3+vj_Tk2}-v*I(i;npRR@3+;8E6iwM-UY_JH9h6O`p1Q~?8gw*S@S-6-&$_k~nS}S> zW`H3s{A%)}Pt^5`wnpr;%Oo{%JC2p27@Fz2DNfCBvyr3e~ad6y|kDYEjb>Av=Tt;c0W^#GP;@?p9NVR z`c>SNT0KT7;Kkc96JnRB&Z0ce7-(0RBHGt{YquPF%`#+q+nzgEMOqaN(`YM(Wu(sZ z^A~TF_}64VWKZ&$x7jT4o8muC--D&5rqIw=hg^zt{`>On`B=a9|1K!&2vT`f@vJe1 zM(o`m&8#|S)_qhVz~k#|fnm4d!&(;YA%jXVW-T!33OE{YFN*=WlUtV}PIzREyCvf^GSleaA@`G2%ddP+L#hVbR`_ZB17dq) zWmjpdcIMY#G@DBV>m;rC-+zm!LJ?^GN)e0;VS}OsQ_kceQ#6O%uK6H8)IwLS%DZfR z)h2I%T&eZ4D#DYn?q_1E8aZ5e)5c(`lSd2~8VZNqSKaI@2HrM6KO3g@A2CpBF2VFS zbg*`QpH@X!6`PjHba?n*Y{ZI$UlB|ROo6$s#EMUeD*4jIZDbQ~Ks65QjMi4Uq$UAa z{g`r(9#K1IV_Pc!l!u(8A2selo3isqE?)9|JnNmJL$j52)O?wF;}`u&eg-@L>E`0h z&w0O_wol8y(@5bi_Ecx;U#FFnc2z~(KCN4I8T66J4%k95yVb0D#-k~w$_%b7uRkRk zV%xU%tPljL9C@Phu*b38l$&5+Fvmk^wKAt}hpk`oZYYhGPwuc12o_ntZqLO=PT60( zVUMsj+SZ402edHc9lFJ^VeB95--BxedOo;rN`XC+fF}_Ta8b}j6RYI#$hNF# z%$_b}ZTe2D9>y7@@uebcdR)GsvbIt~lO3|>y$WtU!BS#`Vw9VCl zD^?zCKp4;Eo)|hbuwm|)(qjC!N5fNe>yny~B(Fbk^l&rKz}KC3{CKN!ogGYSJmNli zq4R8vAcznaSwhR!ae(v` zs5P?hRFW8~tVI;WNP=K7L4+V+SWa45j4U}o1fs&+mjw$0XYdban*Va%yZ3$f-fzA4 ze%~(&w!JR-c)99F{2K%Qb1l!?%?%T9Z?c8~iLy3V&025u zWO`GHL&6}H=;nL8HrEM3Pn0AqEiOQ&)n4=p?1h~?6I~!k`yfI2@#JGR z3tY?v6L3I6;m$Y)f_F(fZxU?cifwfhqC#o#vv#m&2AXF$R@`Gg+Hnvjhn3j2^p&;^ zBkcu>rosEiq}k#DYb27>;+yAR6^c9kB2M9hrt%&8gJG^n`mDMfz}v)?}3CFHARo+>ur_|@E=F29tjNt@uy`*|Trnrv2O4}u{$xD$1~@{eJ?J*w3a zabtb@iIbkFQ$D|m1gt#Af1+!9I<5r`_&sOF8JaZrL=;pFgYzMMa5iMmS0ZdL+G$;& z#@LN`giYe@n zR#khQ{I~hW> z11wK*^1~u#W8be!Rwke36Xw#9*gN$C%*Xv5MBMMCEy#dl3R4#Y+6KQMaZUq)E_Jsh zEf0p3|TM!DHFa7xo0z$H3YdPrPEi{k-luHnjlI;I5L#E5d(GVvKQ1k z0w0wfQ?FwJHmID|nZ%7L>~$Ldbw^tUFBo0UFhUnvnn?|xx3Mv#pUH3Xq~7@qchdog z-!iV^y&Ybb{{vde62M8+OoNz>b_<~@5K>lTN~38R!)|Q-%jhGyG0WYh3=^PPqd5dY z9^M+}J;5rqU6$Sj>P0+}jvfw@-~dJY$$HuDGEc;NWiA<6bkSP$51E%j2M}>1Ql!dt zzO@c#Zfsbq8q*eA{0AXR4NU$woCM4eWJ$rF;S=h7e1LeUvDH|Lvur9?x|(Fsmu@n_ z4waLG8j@Yq`BFCy{qb+TEC}2avyh!BDMciVRFvBT&0iuPF=GK9kzBM)jUj*JO_`#V zpIA>%9?FG~RcPr_7#Fe#Y*5J5iUeKdoieu%(`RqTHSS1f z*?J@2fs?$7IadiRkf8t>K&x+)gQtuqSBZC@za&*NCr83_QjU{x%IXCIo92%sSEDX& zk&}4xQkh--=7yq^|D$PoyQN$iRJ|qH@0R^LdwW$vR?*i@Y>@_dhe5Ti+p@)td`%BO z`v*5GKl@NVsF-5}!Pm-S(^-DEE(8pkV8Rxo!VX0C094qyCt^_qJI+e1ad1P)D7z|0 z0`^O*sHl4=j$fz4q{{3FjsmKqNl0ni_xA%rA#Z8ZAnL9!-th;L9QfHEbvjax!e00{ D_vt({ literal 0 HcmV?d00001 -- 2.45.2