FFMPEG auto download

This commit is contained in:
Stash Dev
2019-02-10 22:39:21 -08:00
parent 57307bfd01
commit 2565173105
16 changed files with 394 additions and 232 deletions

View File

@@ -2,7 +2,7 @@
[![Build Status](https://travis-ci.org/stashapp/stash.svg?branch=master)](https://travis-ci.org/stashapp/stash) [![Build Status](https://travis-ci.org/stashapp/stash.svg?branch=master)](https://travis-ci.org/stashapp/stash)
**Stash is a rails app which organizes and serves your porn.** **Stash is a Go app which organizes and serves your porn.**
See a demo [here](https://vimeo.com/275537038) (password is stashapp). See a demo [here](https://vimeo.com/275537038) (password is stashapp).
@@ -15,18 +15,26 @@ TODO: This is not final. There is more work to be done to ease this process.
### OSX / Linux ### OSX / Linux
1. `mkdir ~/.stash` && `cd ~/.stash` 1. `mkdir ~/.stash` && `cd ~/.stash`
2. Download FFMPEG ([macOS](https://ffmpeg.zeranoe.com/builds/macos64/static/ffmpeg-4.0-macos64-static.zip), [Linux](https://www.johnvansickle.com/ffmpeg/old-releases/ffmpeg-4.0.3-64bit-static.tar.xz)) and extract so that just `ffmpeg` and `ffprobe` are in `~/.stash` 2. Create a `config.json` file (see below).
3. Create a `config.json` file (see below). 3. Run stash with `./stash` and visit `http://localhost:9998` or `https://localhost:9999`
4. Run stash with `./stash` and visit `http://localhost:9998` or `https://localhost:9999`
### Windows ### Windows
1. Create a new folder at `C:\Users\YourUsername\.stash` 1. Create a new folder at `C:\Users\YourUsername\.stash`
2. Download [FFMPEG](https://ffmpeg.zeranoe.com/builds/win64/static/ffmpeg-4.0-win64-static.zip) and extract so that just `ffmpeg.exe` and `ffprobe.exe` are in `C:\Users\YourUsername\.stash` 2. Create a `config.json` file (see below)
3. Create a `config.json` file (see below) 3. Run stash with `./stash` and visit `http://localhost:9998` or `https://localhost:9999`
4. Run stash with `./stash` and visit `http://localhost:9998` or `https://localhost:9999`
### Config.json #### FFMPEG
If stash is unable to find or download FFMPEG then download it yourself from the link for your platform:
* [macOS](https://ffmpeg.zeranoe.com/builds/macos64/static/ffmpeg-4.0-macos64-static.zip)
* [Windows](https://ffmpeg.zeranoe.com/builds/win64/static/ffmpeg-4.0-win64-static.zip)
* [Linux](https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz)
The `ffmpeg(.exe)` and `ffprobe(.exe)` files should be placed in `~/.stash` on macOS / Linux or `C:\Users\YourUsername\.stash` on Windows.
#### Config.json
Example: Example:

168
ffmpeg/downloader.go Normal file
View File

@@ -0,0 +1,168 @@
package ffmpeg
import (
"archive/zip"
"fmt"
"github.com/stashapp/stash/utils"
"io"
"net/http"
"os"
"os/exec"
"path"
"path/filepath"
"runtime"
"strings"
)
func GetPaths(configDirectory string) (string, string) {
var ffmpegPath, ffprobePath string
// Check if ffmpeg exists in the PATH
if pathBinaryHasCorrectFlags() {
ffmpegPath, _ = exec.LookPath("ffmpeg")
ffprobePath, _ = exec.LookPath("ffprobe")
}
// Check if ffmpeg exists in the config directory
ffmpegConfigPath := filepath.Join(configDirectory, getFFMPEGFilename())
ffprobeConfigPath := filepath.Join(configDirectory, getFFProbeFilename())
ffmpegConfigExists, _ := utils.FileExists(ffmpegConfigPath)
ffprobeConfigExists, _ := utils.FileExists(ffprobeConfigPath)
if ffmpegPath == "" && ffmpegConfigExists {
ffmpegPath = ffmpegConfigPath
}
if ffprobePath == "" && ffprobeConfigExists {
ffprobePath = ffprobeConfigPath
}
return ffmpegPath, ffprobePath
}
func Download(configDirectory string) error {
url := getFFMPEGURL()
if url == "" {
return fmt.Errorf("no ffmpeg url for this platform")
}
// Configure where we want to download the archive
urlExt := path.Ext(url)
archivePath := filepath.Join(configDirectory, "ffmpeg"+urlExt)
_ = os.Remove(archivePath) // remove archive if it already exists
out, err := os.Create(archivePath)
if err != nil {
return err
}
defer out.Close()
// Make the HTTP request
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
// Check server response
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("bad status: %s", resp.Status)
}
// Write the response to the archive file location
_, err = io.Copy(out, resp.Body)
if err != nil {
return err
}
if urlExt == ".zip" {
if err := unzip(archivePath, configDirectory); err != nil {
return err
}
} else {
return fmt.Errorf("FFMPeg was downloaded to %s. ")
}
return nil
}
func getFFMPEGURL() string {
switch runtime.GOOS {
case "darwin":
return "https://ffmpeg.zeranoe.com/builds/macos64/static/ffmpeg-4.1-macos64-static.zip"
case "linux":
// TODO: untar this
//return "https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz"
return ""
case "windows":
return "https://ffmpeg.zeranoe.com/builds/win64/static/ffmpeg-4.1-win64-static.zip"
default:
return ""
}
}
func getFFMPEGFilename() string {
if runtime.GOOS == "windows" {
return "ffmpeg.exe"
} else {
return "ffmpeg"
}
}
func getFFProbeFilename() string {
if runtime.GOOS == "windows" {
return "ffprobe.exe"
} else {
return "ffprobe"
}
}
// Checks if FFMPEG in the path has the correct flags
func pathBinaryHasCorrectFlags() bool {
ffmpegPath, err := exec.LookPath("ffmpeg")
if err != nil {
return false
}
bytes, _ := exec.Command(ffmpegPath).CombinedOutput()
output := string(bytes)
hasOpus := strings.Contains(output, "--enable-libopus")
hasVpx := strings.Contains(output, "--enable-libvpx")
hasX264 := strings.Contains(output, "--enable-libx264")
hasX265 := strings.Contains(output, "--enable-libx265")
hasWebp := strings.Contains(output, "--enable-libwebp")
return hasOpus && hasVpx && hasX264 && hasX265 && hasWebp
}
func unzip(src, configDirectory string) error {
zipReader, err := zip.OpenReader(src)
if err != nil {
return err
}
defer zipReader.Close()
for _, f := range zipReader.File {
if f.FileInfo().IsDir() {
continue
}
filename := f.FileInfo().Name()
if filename != "ffprobe" && filename != "ffmpeg" && filename != "ffprobe.exe" && filename != "ffmpeg.exe" {
continue
}
rc, err := f.Open()
unzippedPath := filepath.Join(configDirectory, filename)
unzippedOutput, err := os.Create(unzippedPath)
if err != nil {
return err
}
_, err = io.Copy(unzippedOutput, rc)
if err != nil {
return err
}
if err := unzippedOutput.Close(); err != nil {
return err
}
}
return nil
}

View File

@@ -18,7 +18,7 @@ func main() {
//fmt.Println("hello world") //fmt.Println("hello world")
managerInstance := manager.Initialize() managerInstance := manager.Initialize()
database.Initialize(managerInstance.Paths.FixedPaths.DatabaseFile) database.Initialize(managerInstance.StaticPaths.DatabaseFile)
api.Start() api.Start()
} }

View File

@@ -43,7 +43,7 @@ func NewPreviewGenerator(videoFile ffmpeg.VideoFile, videoFilename string, image
func (g *PreviewGenerator) Generate() error { func (g *PreviewGenerator) Generate() error {
instance.Paths.Generated.EmptyTmpDir() instance.Paths.Generated.EmptyTmpDir()
logger.Infof("[generator] generating scene preview for %s", g.Info.VideoFile.Path) logger.Infof("[generator] generating scene preview for %s", g.Info.VideoFile.Path)
encoder := ffmpeg.NewEncoder(instance.Paths.FixedPaths.FFMPEG) encoder := ffmpeg.NewEncoder(instance.StaticPaths.FFMPEG)
if err := g.generateConcatFile(); err != nil { if err := g.generateConcatFile(); err != nil {
return err return err

View File

@@ -49,7 +49,7 @@ func NewSpriteGenerator(videoFile ffmpeg.VideoFile, imageOutputPath string, vttO
func (g *SpriteGenerator) Generate() error { func (g *SpriteGenerator) Generate() error {
instance.Paths.Generated.EmptyTmpDir() instance.Paths.Generated.EmptyTmpDir()
encoder := ffmpeg.NewEncoder(instance.Paths.FixedPaths.FFMPEG) encoder := ffmpeg.NewEncoder(instance.StaticPaths.FFMPEG)
if err := g.generateSpriteImage(&encoder); err != nil { if err := g.generateSpriteImage(&encoder); err != nil {
return err return err

View File

@@ -1,17 +1,16 @@
package manager package manager
import ( import (
"github.com/bmatcuk/doublestar" "github.com/stashapp/stash/ffmpeg"
"github.com/stashapp/stash/logger" "github.com/stashapp/stash/logger"
"github.com/stashapp/stash/manager/paths" "github.com/stashapp/stash/manager/paths"
"github.com/stashapp/stash/models"
"path/filepath"
"sync" "sync"
) )
type singleton struct { type singleton struct {
Status JobStatus Status JobStatus
Paths *paths.Paths Paths *paths.Paths
StaticPaths *paths.StaticPathsType
JSON *jsonUtils JSON *jsonUtils
} }
@@ -28,126 +27,29 @@ func Initialize() *singleton {
instance = &singleton{ instance = &singleton{
Status: Idle, Status: Idle,
Paths: paths.RefreshPaths(), Paths: paths.RefreshPaths(),
StaticPaths: &paths.StaticPaths,
JSON: &jsonUtils{}, JSON: &jsonUtils{},
} }
initFFMPEG()
}) })
return instance return instance
} }
func (s *singleton) Scan() { func initFFMPEG() {
if s.Status != Idle { return } ffmpegPath, ffprobePath := ffmpeg.GetPaths(instance.StaticPaths.ConfigDirectory)
s.Status = Scan if ffmpegPath == "" || ffprobePath == "" {
logger.Infof("couldn't find FFMPEG, attempting to download it")
if err := ffmpeg.Download(instance.StaticPaths.ConfigDirectory); err != nil {
msg := `Unable to locate / automatically download FFMPEG
go func() { Check the readme for download links.
defer s.returnToIdleState() The FFMPEG and FFProbe binaries should be placed in %s
globPath := filepath.Join(s.Paths.Config.Stash, "**/*.{zip,m4v,mp4,mov,wmv}") The error was: %s
globResults, _ := doublestar.Glob(globPath) `
logger.Infof("Starting scan of %d files", len(globResults)) logger.Fatalf(msg, instance.StaticPaths.ConfigDirectory, err)
}
var wg sync.WaitGroup
for _, path := range globResults {
wg.Add(1)
task := ScanTask{FilePath: path}
go task.Start(&wg)
wg.Wait()
} }
}()
}
func (s *singleton) Import() {
if s.Status != Idle { return }
s.Status = Import
go func() {
defer s.returnToIdleState()
var wg sync.WaitGroup
wg.Add(1)
task := ImportTask{}
go task.Start(&wg)
wg.Wait()
}()
}
func (s *singleton) Export() {
if s.Status != Idle { return }
s.Status = Export
go func() {
defer s.returnToIdleState()
var wg sync.WaitGroup
wg.Add(1)
task := ExportTask{}
go task.Start(&wg)
wg.Wait()
}()
}
func (s *singleton) Generate(sprites bool, previews bool, markers bool, transcodes bool) {
if s.Status != Idle { return }
s.Status = Generate
qb := models.NewSceneQueryBuilder()
//this.job.total = await ObjectionUtils.getCount(Scene);
instance.Paths.Generated.EnsureTmpDir()
go func() {
defer s.returnToIdleState()
scenes, err := qb.All()
if err != nil {
logger.Errorf("failed to get scenes for generate")
return
}
delta := btoi(sprites) + btoi(previews) + btoi(markers) + btoi(transcodes)
var wg sync.WaitGroup
for _, scene := range scenes {
wg.Add(delta)
if sprites {
task := GenerateSpriteTask{Scene: scene}
go task.Start(&wg)
}
if previews {
task := GeneratePreviewTask{Scene: scene}
go task.Start(&wg)
}
if markers {
task := GenerateMarkersTask{Scene: scene}
go task.Start(&wg)
}
if transcodes {
go func() {
wg.Done() // TODO
}()
}
wg.Wait()
}
}()
}
func (s *singleton) returnToIdleState() {
if r := recover(); r!= nil {
logger.Info("recovered from ", r)
}
if s.Status == Generate {
instance.Paths.Generated.RemoveTmpDir()
}
s.Status = Idle
}
func btoi(b bool) int {
if b {
return 1
}
return 0
} }

120
manager/manager_tasks.go Normal file
View File

@@ -0,0 +1,120 @@
package manager
import (
"github.com/bmatcuk/doublestar"
"github.com/stashapp/stash/logger"
"github.com/stashapp/stash/models"
"github.com/stashapp/stash/utils"
"path/filepath"
"sync"
)
func (s *singleton) Scan() {
if s.Status != Idle { return }
s.Status = Scan
go func() {
defer s.returnToIdleState()
globPath := filepath.Join(s.Paths.Config.Stash, "**/*.{zip,m4v,mp4,mov,wmv}")
globResults, _ := doublestar.Glob(globPath)
logger.Infof("Starting scan of %d files", len(globResults))
var wg sync.WaitGroup
for _, path := range globResults {
wg.Add(1)
task := ScanTask{FilePath: path}
go task.Start(&wg)
wg.Wait()
}
}()
}
func (s *singleton) Import() {
if s.Status != Idle { return }
s.Status = Import
go func() {
defer s.returnToIdleState()
var wg sync.WaitGroup
wg.Add(1)
task := ImportTask{}
go task.Start(&wg)
wg.Wait()
}()
}
func (s *singleton) Export() {
if s.Status != Idle { return }
s.Status = Export
go func() {
defer s.returnToIdleState()
var wg sync.WaitGroup
wg.Add(1)
task := ExportTask{}
go task.Start(&wg)
wg.Wait()
}()
}
func (s *singleton) Generate(sprites bool, previews bool, markers bool, transcodes bool) {
if s.Status != Idle { return }
s.Status = Generate
qb := models.NewSceneQueryBuilder()
//this.job.total = await ObjectionUtils.getCount(Scene);
instance.Paths.Generated.EnsureTmpDir()
go func() {
defer s.returnToIdleState()
scenes, err := qb.All()
if err != nil {
logger.Errorf("failed to get scenes for generate")
return
}
delta := utils.Btoi(sprites) + utils.Btoi(previews) + utils.Btoi(markers) + utils.Btoi(transcodes)
var wg sync.WaitGroup
for _, scene := range scenes {
wg.Add(delta)
if sprites {
task := GenerateSpriteTask{Scene: scene}
go task.Start(&wg)
}
if previews {
task := GeneratePreviewTask{Scene: scene}
go task.Start(&wg)
}
if markers {
task := GenerateMarkersTask{Scene: scene}
go task.Start(&wg)
}
if transcodes {
go func() {
wg.Done() // TODO
}()
}
wg.Wait()
}
}()
}
func (s *singleton) returnToIdleState() {
if r := recover(); r!= nil {
logger.Info("recovered from ", r)
}
if s.Status == Generate {
instance.Paths.Generated.RemoveTmpDir()
}
s.Status = Idle
}

View File

@@ -3,13 +3,9 @@ package paths
import ( import (
"github.com/stashapp/stash/manager/jsonschema" "github.com/stashapp/stash/manager/jsonschema"
"github.com/stashapp/stash/utils" "github.com/stashapp/stash/utils"
"os"
"os/user"
"path/filepath"
) )
type Paths struct { type Paths struct {
FixedPaths *fixedPaths
Config *jsonschema.Config Config *jsonschema.Config
Generated *generatedPaths Generated *generatedPaths
JSON *jsonPaths JSON *jsonPaths
@@ -20,15 +16,13 @@ type Paths struct {
} }
func RefreshPaths() *Paths { func RefreshPaths() *Paths {
fp := newFixedPaths() ensureConfigFile()
ensureConfigFile(fp) return newPaths()
return newPaths(fp)
} }
func newPaths(fp *fixedPaths) *Paths { func newPaths() *Paths {
p := Paths{} p := Paths{}
p.FixedPaths = fp p.Config = jsonschema.LoadConfigFile(StaticPaths.ConfigFile)
p.Config = jsonschema.LoadConfigFile(p.FixedPaths.ConfigFile)
p.Generated = newGeneratedPaths(p) p.Generated = newGeneratedPaths(p)
p.JSON = newJSONPaths(p) p.JSON = newJSONPaths(p)
@@ -38,24 +32,8 @@ func newPaths(fp *fixedPaths) *Paths {
return &p return &p
} }
func getExecutionDirectory() string { func ensureConfigFile() {
ex, err := os.Executable() configFileExists, _ := utils.FileExists(StaticPaths.ConfigFile) // TODO: Verify JSON is correct. Pass verified
if err != nil {
panic(err)
}
return filepath.Dir(ex)
}
func getHomeDirectory() string {
currentUser, err := user.Current()
if err != nil {
panic(err)
}
return currentUser.HomeDir
}
func ensureConfigFile(fp *fixedPaths) {
configFileExists, _ := utils.FileExists(fp.ConfigFile) // TODO: Verify JSON is correct. Pass verified
if configFileExists { if configFileExists {
return return
} }

View File

@@ -1,66 +0,0 @@
package paths
import (
"fmt"
"github.com/stashapp/stash/utils"
"path/filepath"
"runtime"
"strings"
)
type fixedPaths struct {
ExecutionDirectory string
ConfigDirectory string
ConfigFile string
DatabaseFile string
FFMPEG string
FFProbe string
}
func newFixedPaths() *fixedPaths {
fp := fixedPaths{}
fp.ExecutionDirectory = getExecutionDirectory()
fp.ConfigDirectory = filepath.Join(getHomeDirectory(), ".stash")
fp.ConfigFile = filepath.Join(fp.ConfigDirectory, "config.json")
fp.DatabaseFile = filepath.Join(fp.ConfigDirectory, "stash-go.sqlite")
ffmpegDirectories := []string{fp.ExecutionDirectory, fp.ConfigDirectory}
ffmpegFileName := func() string {
if runtime.GOOS == "windows" {
return "ffmpeg.exe"
} else {
return "ffmpeg"
}
}()
ffprobeFileName := func() string {
if runtime.GOOS == "windows" {
return "ffprobe.exe"
} else {
return "ffprobe"
}
}()
for _, directory := range ffmpegDirectories {
ffmpegPath := filepath.Join(directory, ffmpegFileName)
ffprobePath := filepath.Join(directory, ffprobeFileName)
if exists, _ := utils.FileExists(ffmpegPath); exists {
fp.FFMPEG = ffmpegPath
}
if exists, _ := utils.FileExists(ffprobePath); exists {
fp.FFProbe = ffprobePath
}
}
errorText := fmt.Sprintf(
"FFMPEG or FFProbe not found. Place it in one of the following folders:\n\n%s",
strings.Join(ffmpegDirectories, ","),
)
if exists, _ := utils.FileExists(fp.FFMPEG); !exists {
panic(errorText)
}
if exists, _ := utils.FileExists(fp.FFProbe); !exists {
panic(errorText)
}
return &fp
}

View File

@@ -0,0 +1,44 @@
package paths
import (
"os"
"os/user"
"path/filepath"
)
type StaticPathsType struct {
ExecutionDirectory string
ConfigDirectory string
ConfigFile string
DatabaseFile string
FFMPEG string
FFProbe string
}
var StaticPaths = StaticPathsType{
ExecutionDirectory: getExecutionDirectory(),
ConfigDirectory: getConfigDirectory(),
ConfigFile: filepath.Join(getConfigDirectory(), "config.json"),
DatabaseFile: filepath.Join(getConfigDirectory(), "stash-go.sqlite"),
}
func getExecutionDirectory() string {
ex, err := os.Executable()
if err != nil {
panic(err)
}
return filepath.Dir(ex)
}
func getHomeDirectory() string {
currentUser, err := user.Current()
if err != nil {
panic(err)
}
return currentUser.HomeDir
}
func getConfigDirectory() string {
return filepath.Join(getHomeDirectory(), ".stash")
}

View File

@@ -25,7 +25,7 @@ func (t *GenerateMarkersTask) Start(wg *sync.WaitGroup) {
return return
} }
videoFile, err := ffmpeg.NewVideoFile(instance.Paths.FixedPaths.FFProbe, t.Scene.Path) videoFile, err := ffmpeg.NewVideoFile(instance.StaticPaths.FFProbe, t.Scene.Path)
if err != nil { if err != nil {
logger.Errorf("error reading video file: %s", err.Error()) logger.Errorf("error reading video file: %s", err.Error())
return return
@@ -35,7 +35,7 @@ func (t *GenerateMarkersTask) Start(wg *sync.WaitGroup) {
markersFolder := filepath.Join(instance.Paths.Generated.Markers, t.Scene.Checksum) markersFolder := filepath.Join(instance.Paths.Generated.Markers, t.Scene.Checksum)
_ = utils.EnsureDir(markersFolder) _ = utils.EnsureDir(markersFolder)
encoder := ffmpeg.NewEncoder(instance.Paths.FixedPaths.FFMPEG) encoder := ffmpeg.NewEncoder(instance.StaticPaths.FFMPEG)
for i, sceneMarker := range sceneMarkers { for i, sceneMarker := range sceneMarkers {
index := i + 1 index := i + 1
logger.Progressf("[generator] <%s> scene marker %d of %d", t.Scene.Checksum, index, len(sceneMarkers)) logger.Progressf("[generator] <%s> scene marker %d of %d", t.Scene.Checksum, index, len(sceneMarkers))

View File

@@ -21,7 +21,7 @@ func (t *GeneratePreviewTask) Start(wg *sync.WaitGroup) {
return return
} }
videoFile, err := ffmpeg.NewVideoFile(instance.Paths.FixedPaths.FFProbe, t.Scene.Path) videoFile, err := ffmpeg.NewVideoFile(instance.StaticPaths.FFProbe, t.Scene.Path)
if err != nil { if err != nil {
logger.Errorf("error reading video file: %s", err.Error()) logger.Errorf("error reading video file: %s", err.Error())
return return

View File

@@ -19,7 +19,7 @@ func (t *GenerateSpriteTask) Start(wg *sync.WaitGroup) {
return return
} }
videoFile, err := ffmpeg.NewVideoFile(instance.Paths.FixedPaths.FFProbe, t.Scene.Path) videoFile, err := ffmpeg.NewVideoFile(instance.StaticPaths.FFProbe, t.Scene.Path)
if err != nil { if err != nil {
logger.Errorf("error reading video file: %s", err.Error()) logger.Errorf("error reading video file: %s", err.Error())
return return

View File

@@ -33,7 +33,7 @@ func (t *ImportTask) Start(wg *sync.WaitGroup) {
} }
t.Scraped = scraped t.Scraped = scraped
database.Reset(instance.Paths.FixedPaths.DatabaseFile) database.Reset(instance.StaticPaths.DatabaseFile)
ctx := context.TODO() ctx := context.TODO()

View File

@@ -70,7 +70,7 @@ func (t *ScanTask) scanGallery() {
} }
func (t *ScanTask) scanScene() { func (t *ScanTask) scanScene() {
videoFile, err := ffmpeg.NewVideoFile(instance.Paths.FixedPaths.FFProbe, t.FilePath) videoFile, err := ffmpeg.NewVideoFile(instance.StaticPaths.FFProbe, t.FilePath)
if err != nil { if err != nil {
logger.Error(err.Error()) logger.Error(err.Error())
return return
@@ -142,7 +142,7 @@ func (t *ScanTask) makeScreenshots(probeResult ffmpeg.VideoFile, checksum string
} }
func (t *ScanTask) makeScreenshot(probeResult ffmpeg.VideoFile, outputPath string, quality int, width int) { func (t *ScanTask) makeScreenshot(probeResult ffmpeg.VideoFile, outputPath string, quality int, width int) {
encoder := ffmpeg.NewEncoder(instance.Paths.FixedPaths.FFMPEG) encoder := ffmpeg.NewEncoder(instance.StaticPaths.FFMPEG)
options := ffmpeg.ScreenshotOptions{ options := ffmpeg.ScreenshotOptions{
OutputPath: outputPath, OutputPath: outputPath,
Quality: quality, Quality: quality,

8
utils/boolean.go Normal file
View File

@@ -0,0 +1,8 @@
package utils
func Btoi(b bool) int {
if b {
return 1
}
return 0
}