diff --git a/avrp.go b/avrp.go index b16a7c1..5b893ab 100644 --- a/avrp.go +++ b/avrp.go @@ -56,27 +56,24 @@ func main() { flag.BoolVar(&reset, "reset", false, "removes all configs, thumbnails & 'aframe-vr-player' files") flag.IntVar(&port, "port", 5000, "port to serve on (default 5000)") flag.BoolVar(&getFfmpeg, "get-ffmpeg", false, "downloads ffmpeg") - flag.BoolVar(&noThumb, "no-thumb", false, "disables thumbnail generation") + flag.BoolVar(&noThumb, "no-thumb", false, "disable/enable thumbnail generation") flag.Parse() utils.Init() - dist.Init() - thumbnails.Init() // commands πŸ‘‡ - if getFfmpeg { utils.Panic(thumbnails.DownloadFfmpeg()) return } if noThumb { - if thumbnails.NoFfmpeg() { - thumbnails.NoFfmpegFileRemove() + if thumbnails.NoThumb() { + thumbnails.NoThumbFileRemove() log.Println("thumbnail generation enabledπŸ‘") } else { - thumbnails.NoFfmpegFileCreate() + thumbnails.NoThumbFileCreate() log.Println("thumbnail generation disabledπŸ‘Ž") } return @@ -96,7 +93,6 @@ func main() { dist.Update(sha) return } - // commands πŸ‘† // running for the first time @@ -112,6 +108,7 @@ func main() { } } + thumbnails.Init() server.Init(servDir) server.Start(port) } diff --git a/internal/dist/dist.go b/internal/dist/dist.go index 5544615..fb655fa 100644 --- a/internal/dist/dist.go +++ b/internal/dist/dist.go @@ -8,12 +8,6 @@ import ( "github.com/mysterion/avrp/internal/utils" ) -var VersionFile string - -func Init() { - VersionFile = filepath.Join(utils.ConfigDir, "VERSION") -} - // checks if dist is present func Valid() bool { _, err := os.Stat(filepath.Join(utils.DistDir, "index.html")) @@ -26,7 +20,7 @@ func Delete() error { // returns sha of aframe-vr-player dist func Ver() string { - d, err := os.ReadFile(VersionFile) + d, err := os.ReadFile(utils.VersionFile) if err != nil { log.Printf("WARN: while reading dist version, %v\n", err.Error()) diff --git a/internal/dist/download.go b/internal/dist/download.go index 129cdf1..7f5dc43 100644 --- a/internal/dist/download.go +++ b/internal/dist/download.go @@ -43,7 +43,7 @@ func DownloadCommit(sha string) error { return err } - err = os.WriteFile(VersionFile, []byte(sha), 0644) + err = os.WriteFile(utils.VersionFile, []byte(sha), 0644) if err != nil { log.Println("ERR: Failed to write version file") diff --git a/internal/thumbnails/ffmpeg.go b/internal/thumbnails/ffmpeg.go index 2188e51..cb105e9 100644 --- a/internal/thumbnails/ffmpeg.go +++ b/internal/thumbnails/ffmpeg.go @@ -30,11 +30,6 @@ var ( binFfprobe string ) -var ( - noffmpegfile string - ffmpegDir = "" -) - type release struct { ID int `json:"id"` Name string `json:"name"` @@ -143,8 +138,8 @@ func DownloadFfmpeg() error { log.Println("Extracted successfully") - if NoFfmpeg() { - NoFfmpegFileRemove() + if NoThumb() { + NoThumbFileRemove() } return nil @@ -202,7 +197,11 @@ func promptDownloadFfmpeg() bool { return ans == "yes" } -func CheckFfmpegInPath() (bool, string, string) { +func initFfmpeg() bool { + + if NoThumb() { + return false + } ffmpeg := "ffmpeg" ffprobe := "ffprobe" @@ -212,43 +211,56 @@ func CheckFfmpegInPath() (bool, string, string) { ffprobe += ".exe" } - ffmpegPath, err1 := exec.LookPath(ffmpeg) + var err1, err2 error - ffprobePath, err2 := exec.LookPath(ffprobe) + binFfmpeg, err1 = exec.LookPath(ffmpeg) + binFfprobe, err2 = exec.LookPath(ffprobe) - return err1 == nil && err2 == nil, ffmpegPath, ffprobePath -} + if err1 == nil && err2 == nil { + log.Println("ffmpeg found in $PATH") + return true + } -func CheckFfmpeg() (bool, string, string) { + binFfmpeg = filepath.Join(utils.FfmpegDir, "bin", ffmpeg) + binFfprobe = filepath.Join(utils.FfmpegDir, "bin", ffprobe) - ffmpeg := filepath.Join("bin", "ffmpeg") - ffprobe := filepath.Join("bin", "ffprobe") + _, err1 = os.Stat(binFfmpeg) + _, err2 = os.Stat(binFfprobe) - if runtime.GOOS == "windows" { - ffmpeg += ".exe" - ffprobe += ".exe" + if err1 == nil && err2 == nil { + log.Println("ffmpeg found in the ffmpegDir") + return true } - ffmpegPath := filepath.Join(ffmpegDir, ffmpeg) - ffprobePath := filepath.Join(ffmpegDir, ffprobe) + // code to download ffmpeg download πŸ‘‡πŸ‘‡πŸ‘‡πŸ‘‡ + accept := promptDownloadFfmpeg() + + if !accept { + fmt.Printf("\n\nYou can disable this message, by running: avrp --no-thumb\n\n") + return false + } - _, err1 := os.Stat(ffmpegPath) - _, err2 := os.Stat(ffprobePath) + err := DownloadFfmpeg() + + if err != nil { + log.Println("ERR: Failed to download ffmpeg") + return false + } - return err1 == nil && err2 == nil, ffmpegPath, ffprobePath + return true } -func NoFfmpegFileCreate() { - _, err := os.Create(noffmpegfile) +func NoThumbFileCreate() { + _, err := os.Create(utils.NoThumbFile) utils.Panic(err) } -func NoFfmpegFileRemove() { - err := os.Remove(noffmpegfile) +func NoThumbFileRemove() { + err := os.Remove(utils.NoThumbFile) utils.Panic(err) } -func NoFfmpeg() bool { - _, err := os.Stat(noffmpegfile) +func NoThumb() bool { + _, err := os.Stat(utils.NoThumbFile) return err == nil } diff --git a/internal/thumbnails/guard.go b/internal/thumbnails/guard.go new file mode 100644 index 0000000..fccb058 --- /dev/null +++ b/internal/thumbnails/guard.go @@ -0,0 +1,76 @@ +package thumbnails + +import ( + "log" + "os" + "runtime" + "strconv" + "sync" +) + +/* Controls the no of concurrent processes used to generate thumbnails */ +type GuardProcs struct { + g chan struct{} + max int +} + +func NewGuardProcs() GuardProcs { + + maxProcs := runtime.NumCPU() + maxProcsEnv := os.Getenv("AVRP_MAX_PROCS") + if maxProcsEnv != "" { + mp, err := strconv.Atoi(maxProcsEnv) + if err == nil { + maxProcs = mp + } + } + + log.Printf("Using %v threads for thumbnail generation\n", maxProcs) + + return GuardProcs{ + g: make(chan struct{}, maxProcs), + max: maxProcs, + } + +} + +func (p *GuardProcs) MaxProcs() int { + return p.max +} + +func (p *GuardProcs) In() { + p.g <- struct{}{} +} + +func (p *GuardProcs) Out() { + <-p.g +} + +type GuardFile struct { + locks map[string]*sync.Mutex +} + +func NewGuardFile() GuardFile { + return GuardFile{ + locks: make(map[string]*sync.Mutex), + } +} + +func (p *GuardFile) Lock(key string) { + l, exists := p.locks[key] + if exists { + l.Lock() + } else { + p.locks[key] = &sync.Mutex{} + p.locks[key].Lock() + } +} + +func (p *GuardFile) Unlock(key string) { + l, exists := p.locks[key] + if exists { + l.Unlock() + } else { + log.Printf("ERR: %s not found in GuardFile\n", key) + } +} diff --git a/internal/thumbnails/thumbnails.go b/internal/thumbnails/thumbnails.go index 40a1208..250f38e 100644 --- a/internal/thumbnails/thumbnails.go +++ b/internal/thumbnails/thumbnails.go @@ -10,7 +10,6 @@ import ( "path/filepath" "strconv" "strings" - "sync" "github.com/mysterion/avrp/internal/cache" "github.com/mysterion/avrp/internal/utils" @@ -18,67 +17,31 @@ import ( var thumbdir string -var ErrUnavailable = errors.New("not available") +var ErrNotVideo = errors.New("not a video") var Available = false -var muGen sync.Mutex +var ( + guardFile GuardFile + guardProcs GuardProcs +) func Init() { - noffmpegfile = filepath.Join(utils.ConfigDir, "noffmpeg") - - thumbdir = filepath.Join(utils.ConfigDir, "thumbnails") - err := os.MkdirAll(thumbdir, 0755) - if err != nil { - log.Printf("ERR: Thumbnails not available - %v\n", err) - return - } - - ffmpegDir = filepath.Join(utils.ConfigDir, "ffmpeg") - err = os.MkdirAll(ffmpegDir, 0755) - if err != nil { - log.Printf("ERR: Thumbnails not available - %v\n", err) - return - } - - if NoFfmpeg() { - return - } - - var found = false - found, binFfmpeg, binFfprobe = CheckFfmpegInPath() - if found { - Available = true - return - } - - found, binFfmpeg, binFfprobe = CheckFfmpeg() - if found { - Available = true - return - } - accept := promptDownloadFfmpeg() + Available = initFfmpeg() - if !accept { - fmt.Printf("\n\nYou can disable this message, by running: avrp --no-thumb\n\n") + if !Available { return } - utils.Panic(DownloadFfmpeg()) + guardFile = NewGuardFile() + guardProcs = NewGuardProcs() - found, binFfmpeg, binFfprobe = CheckFfmpeg() - if found { - Available = true - return - } else { - log.Println("Something went wrong, please re-download ffmpeg: avrp --get-ffmpeg") - } } func GetDuration(file string) (float64, error) { - if !utils.IsVideo(file) || !Available { - return 0, ErrUnavailable + if !utils.IsVideo(file) { + return 0, ErrNotVideo } var secs string secs = cache.Get("DUR_" + file) @@ -105,44 +68,49 @@ func GetDuration(file string) (float64, error) { } func Generated(file string) bool { + guardFile.Lock(file) + defer guardFile.Unlock(file) - if !Available { + if cache.Get("GEN_"+file) != "" { + return true + } + + if !Available || !utils.IsVideo(file) { return false } h, err := Hash(file) if err != nil { - log.Println("ERR - ", err) return false } duration, err := GetDuration(file) if err != nil { - log.Println("ERR - ", err) return false } - p := filepath.Join(thumbdir, h, fmt.Sprintf("%v.jpg", math.Floor(duration/60)-1)) - _, err = os.Stat(p) + count := math.Floor(duration / 60) + + for i := 0; i < int(count); i++ { + t := fmt.Sprintf("%d.jpg", i) + fd, err := os.Stat(filepath.Join(thumbdir, h, t)) + if err != nil || fd.Size() == 0 { + return false + } + } - return err == nil + cache.Set("GEN_"+file, "OK") + return true } -// TODO: keep error state for a particular file with eviction policy func Generate(file string) { - if !Available { - return - } - muGen.Lock() - defer muGen.Unlock() - if Generated(file) { - log.Printf("Already Generated - %v\n", file) - return - } + guardFile.Lock(file) + defer guardFile.Unlock(file) + h, err := Hash(file) if err != nil { - log.Printf("ERR: %v\n", err.Error()) + log.Printf("ERR: while generating file hash - %v\n", err.Error()) } outDir := filepath.Join(thumbdir, h) @@ -163,7 +131,9 @@ func Generate(file string) { defer close(done) for i := 0; i < n; i++ { + guardProcs.In() go func(i int, done chan<- bool) { + defer guardProcs.Out() defer func() { done <- true }() cmdArgs := []string{ "-y", "-accurate_seek", "-ss", fmt.Sprintf("%v", i*60), @@ -173,12 +143,12 @@ func Generate(file string) { filepath.Join(outDir, fmt.Sprintf("%v.jpg", i)), } cmd := exec.Command(binFfmpeg, cmdArgs...) + log.Printf("generating thumbnail %v.jpg :::: %v\n", i, filepath.Base(file)) stdout, err := cmd.CombinedOutput() if err != nil { - log.Printf("ERR while generating thumbnail %vth for %v - %v\nSTDOUT:\n%v\n", i, file, err, string(stdout)) + log.Printf("ERR while generating thumbnail %v.jpg :::: %v - %v\nSTDOUT:\n%v\n", i, file, err, string(stdout)) return } - }(i, done) } @@ -188,9 +158,6 @@ func Generate(file string) { } func Get(id string, file string) (string, error) { - if !Available { - return "", ErrUnavailable - } h, err := Hash(file) if err != nil { return "", err diff --git a/internal/utils/utils.go b/internal/utils/utils.go index 066ba93..a8ea82c 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -8,9 +8,18 @@ import ( "strings" ) -var ConfigDir string -var DistDir string -var AppDir string +var ( + ConfigDir string + DistDir string + AppDir string + FfmpegDir string + ThumbDir string +) + +var ( + NoThumbFile string + VersionFile string +) var DEV bool @@ -21,6 +30,14 @@ func Init() { ConfigDir = filepath.Join(h, ".avrp") + FfmpegDir = filepath.Join(ConfigDir, "ffmpeg") + + ThumbDir = filepath.Join(ConfigDir, "thumbnails") + + NoThumbFile = filepath.Join(ConfigDir, "nothumb") + + VersionFile = filepath.Join(ConfigDir, "VERSION") + DistDir = filepath.Join(ConfigDir, "dist") if DEV { DistDir = "." @@ -36,11 +53,13 @@ func Init() { Panic(err) } - err = os.MkdirAll(ConfigDir, 0755) - Panic(err) + Panic(os.MkdirAll(ConfigDir, 0755)) - err = os.MkdirAll(DistDir, 0755) - Panic(err) + Panic(os.MkdirAll(DistDir, 0755)) + + Panic(os.MkdirAll(FfmpegDir, 0755)) + + Panic(os.MkdirAll(ThumbDir, 0755)) if DEV { log.Printf("Home directory: %s\n", h)