package manager import ( "context" "errors" "fmt" "os" "path/filepath" "strconv" "strings" "sync" "time" "github.com/remeh/sizedwaitgroup" "github.com/stashapp/stash/pkg/autotag" "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/manager/config" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/utils" ) func isGallery(pathname string) bool { gExt := config.GetInstance().GetGalleryExtensions() return matchExtension(pathname, gExt) } func isVideo(pathname string) bool { vidExt := config.GetInstance().GetVideoExtensions() return matchExtension(pathname, vidExt) } func isImage(pathname string) bool { imgExt := config.GetInstance().GetImageExtensions() return matchExtension(pathname, imgExt) } type TaskStatus struct { Status JobStatus Progress float64 LastUpdate time.Time stopping bool upTo int total int } func (t *TaskStatus) Stop() bool { t.stopping = true t.updated() return true } func (t *TaskStatus) SetStatus(s JobStatus) { t.Status = s t.updated() } func (t *TaskStatus) setProgress(upTo int, total int) { if total == 0 { t.Progress = 1 } t.upTo = upTo t.total = total t.Progress = float64(upTo) / float64(total) t.updated() } func (t *TaskStatus) setProgressPercent(progress float64) { if progress != t.Progress { t.Progress = progress t.updated() } } func (t *TaskStatus) incrementProgress() { t.setProgress(t.upTo+1, t.total) } func (t *TaskStatus) indefiniteProgress() { t.Progress = -1 t.updated() } func (t *TaskStatus) updated() { t.LastUpdate = time.Now() } func getScanPaths(inputPaths []string) []*models.StashConfig { if len(inputPaths) == 0 { return config.GetInstance().GetStashPaths() } var ret []*models.StashConfig for _, p := range inputPaths { s := getStashFromDirPath(p) if s == nil { logger.Warnf("%s is not in the configured stash paths", p) continue } // make a copy, changing the path ss := *s ss.Path = p ret = append(ret, &ss) } return ret } func (s *singleton) neededScan(paths []*models.StashConfig) (total *int, newFiles *int) { const timeout = 90 * time.Second // create a control channel through which to signal the counting loop when the timeout is reached chTimeout := time.After(timeout) logger.Infof("Counting files to scan...") t := 0 n := 0 timeoutErr := errors.New("timed out") for _, sp := range paths { err := walkFilesToScan(sp, func(path string, info os.FileInfo, err error) error { t++ task := ScanTask{FilePath: path, TxnManager: s.TxnManager} if !task.doesPathExist() { n++ } //check for timeout select { case <-chTimeout: return timeoutErr default: } // check stop if s.Status.stopping { return timeoutErr } return nil }) if err == timeoutErr { // timeout should return nil counts return nil, nil } if err != nil { logger.Errorf("Error encountered counting files to scan: %s", err.Error()) return nil, nil } } return &t, &n } func (s *singleton) Scan(input models.ScanMetadataInput) { if s.Status.Status != Idle { return } s.Status.SetStatus(Scan) s.Status.indefiniteProgress() go func() { defer s.returnToIdleState() paths := getScanPaths(input.Paths) total, newFiles := s.neededScan(paths) if s.Status.stopping { logger.Info("Stopping due to user request") return } if total == nil || newFiles == nil { logger.Infof("Taking too long to count content. Skipping...") logger.Infof("Starting scan") } else { logger.Infof("Starting scan of %d files. %d New files found", *total, *newFiles) } start := time.Now() config := config.GetInstance() parallelTasks := config.GetParallelTasksWithAutoDetection() logger.Infof("Scan started with %d parallel tasks", parallelTasks) wg := sizedwaitgroup.New(parallelTasks) s.Status.Progress = 0 fileNamingAlgo := config.GetVideoFileNamingAlgorithm() calculateMD5 := config.IsCalculateMD5() i := 0 stoppingErr := errors.New("stopping") var err error var galleries []string for _, sp := range paths { err = walkFilesToScan(sp, func(path string, info os.FileInfo, err error) error { if total != nil { s.Status.setProgress(i, *total) i++ } if s.Status.stopping { return stoppingErr } if isGallery(path) { galleries = append(galleries, path) } instance.Paths.Generated.EnsureTmpDir() wg.Add() task := ScanTask{ TxnManager: s.TxnManager, FilePath: path, UseFileMetadata: utils.IsTrue(input.UseFileMetadata), StripFileExtension: utils.IsTrue(input.StripFileExtension), fileNamingAlgorithm: fileNamingAlgo, calculateMD5: calculateMD5, GeneratePreview: utils.IsTrue(input.ScanGeneratePreviews), GenerateImagePreview: utils.IsTrue(input.ScanGenerateImagePreviews), GenerateSprite: utils.IsTrue(input.ScanGenerateSprites), GeneratePhash: utils.IsTrue(input.ScanGeneratePhashes), } go task.Start(&wg) return nil }) if err == stoppingErr { logger.Info("Stopping due to user request") break } if err != nil { logger.Errorf("Error encountered scanning files: %s", err.Error()) break } } wg.Wait() instance.Paths.Generated.EmptyTmpDir() elapsed := time.Since(start) logger.Info(fmt.Sprintf("Scan finished (%s)", elapsed)) if s.Status.stopping || err != nil { return } for _, path := range galleries { wg.Add() task := ScanTask{ TxnManager: s.TxnManager, FilePath: path, UseFileMetadata: false, } go task.associateGallery(&wg) wg.Wait() } logger.Info("Finished gallery association") }() } func (s *singleton) Import() error { config := config.GetInstance() metadataPath := config.GetMetadataPath() if metadataPath == "" { return errors.New("metadata path must be set in config") } if s.Status.Status != Idle { return nil } s.Status.SetStatus(Import) s.Status.indefiniteProgress() go func() { defer s.returnToIdleState() var wg sync.WaitGroup wg.Add(1) task := ImportTask{ txnManager: s.TxnManager, BaseDir: metadataPath, Reset: true, DuplicateBehaviour: models.ImportDuplicateEnumFail, MissingRefBehaviour: models.ImportMissingRefEnumFail, fileNamingAlgorithm: config.GetVideoFileNamingAlgorithm(), } go task.Start(&wg) wg.Wait() }() return nil } func (s *singleton) Export() error { config := config.GetInstance() metadataPath := config.GetMetadataPath() if metadataPath == "" { return errors.New("metadata path must be set in config") } if s.Status.Status != Idle { return nil } s.Status.SetStatus(Export) s.Status.indefiniteProgress() go func() { defer s.returnToIdleState() var wg sync.WaitGroup wg.Add(1) task := ExportTask{ txnManager: s.TxnManager, full: true, fileNamingAlgorithm: config.GetVideoFileNamingAlgorithm(), } go task.Start(&wg) wg.Wait() }() return nil } func (s *singleton) RunSingleTask(t Task) (*sync.WaitGroup, error) { if s.Status.Status != Idle { return nil, errors.New("task already running") } s.Status.SetStatus(t.GetStatus()) s.Status.indefiniteProgress() var wg sync.WaitGroup wg.Add(1) go func() { defer s.returnToIdleState() go t.Start(&wg) wg.Wait() }() return &wg, nil } func setGeneratePreviewOptionsInput(optionsInput *models.GeneratePreviewOptionsInput) { config := config.GetInstance() if optionsInput.PreviewSegments == nil { val := config.GetPreviewSegments() optionsInput.PreviewSegments = &val } if optionsInput.PreviewSegmentDuration == nil { val := config.GetPreviewSegmentDuration() optionsInput.PreviewSegmentDuration = &val } if optionsInput.PreviewExcludeStart == nil { val := config.GetPreviewExcludeStart() optionsInput.PreviewExcludeStart = &val } if optionsInput.PreviewExcludeEnd == nil { val := config.GetPreviewExcludeEnd() optionsInput.PreviewExcludeEnd = &val } if optionsInput.PreviewPreset == nil { val := config.GetPreviewPreset() optionsInput.PreviewPreset = &val } } func (s *singleton) Generate(input models.GenerateMetadataInput) { if s.Status.Status != Idle { return } s.Status.SetStatus(Generate) s.Status.indefiniteProgress() //this.job.total = await ObjectionUtils.getCount(Scene); instance.Paths.Generated.EnsureTmpDir() sceneIDs, err := utils.StringSliceToIntSlice(input.SceneIDs) if err != nil { logger.Error(err.Error()) } markerIDs, err := utils.StringSliceToIntSlice(input.MarkerIDs) if err != nil { logger.Error(err.Error()) } go func() { defer s.returnToIdleState() var scenes []*models.Scene var err error var markers []*models.SceneMarker if err := s.TxnManager.WithReadTxn(context.TODO(), func(r models.ReaderRepository) error { qb := r.Scene() if len(sceneIDs) > 0 { scenes, err = qb.FindMany(sceneIDs) } else { scenes, err = qb.All() } if err != nil { return err } if len(markerIDs) > 0 { markers, err = r.SceneMarker().FindMany(markerIDs) if err != nil { return err } } return nil }); err != nil { logger.Error(err.Error()) return } config := config.GetInstance() parallelTasks := config.GetParallelTasksWithAutoDetection() logger.Infof("Generate started with %d parallel tasks", parallelTasks) wg := sizedwaitgroup.New(parallelTasks) s.Status.Progress = 0 lenScenes := len(scenes) total := lenScenes + len(markers) if s.Status.stopping { logger.Info("Stopping due to user request") return } totalsNeeded := s.neededGenerate(scenes, input) if totalsNeeded == nil { logger.Infof("Taking too long to count content. Skipping...") logger.Infof("Generating content") } else { logger.Infof("Generating %d sprites %d previews %d image previews %d markers %d transcodes %d phashes", totalsNeeded.sprites, totalsNeeded.previews, totalsNeeded.imagePreviews, totalsNeeded.markers, totalsNeeded.transcodes, totalsNeeded.phashes) } fileNamingAlgo := config.GetVideoFileNamingAlgorithm() overwrite := false if input.Overwrite != nil { overwrite = *input.Overwrite } generatePreviewOptions := input.PreviewOptions if generatePreviewOptions == nil { generatePreviewOptions = &models.GeneratePreviewOptionsInput{} } setGeneratePreviewOptionsInput(generatePreviewOptions) // Start measuring how long the generate has taken. (consider moving this up) start := time.Now() instance.Paths.Generated.EnsureTmpDir() for i, scene := range scenes { s.Status.setProgress(i, total) if s.Status.stopping { logger.Info("Stopping due to user request") wg.Wait() instance.Paths.Generated.EmptyTmpDir() return } if scene == nil { logger.Errorf("nil scene, skipping generate") continue } if input.Sprites { task := GenerateSpriteTask{ Scene: *scene, Overwrite: overwrite, fileNamingAlgorithm: fileNamingAlgo, } wg.Add() go task.Start(&wg) } if input.Previews { task := GeneratePreviewTask{ Scene: *scene, ImagePreview: input.ImagePreviews, Options: *generatePreviewOptions, Overwrite: overwrite, fileNamingAlgorithm: fileNamingAlgo, } wg.Add() go task.Start(&wg) } if input.Markers { wg.Add() task := GenerateMarkersTask{ TxnManager: s.TxnManager, Scene: scene, Overwrite: overwrite, fileNamingAlgorithm: fileNamingAlgo, } go task.Start(&wg) } if input.Transcodes { wg.Add() task := GenerateTranscodeTask{ Scene: *scene, Overwrite: overwrite, fileNamingAlgorithm: fileNamingAlgo, } go task.Start(&wg) } if input.Phashes { task := GeneratePhashTask{ Scene: *scene, fileNamingAlgorithm: fileNamingAlgo, txnManager: s.TxnManager, } wg.Add() go task.Start(&wg) } } wg.Wait() for i, marker := range markers { s.Status.setProgress(lenScenes+i, total) if s.Status.stopping { logger.Info("Stopping due to user request") wg.Wait() instance.Paths.Generated.EmptyTmpDir() elapsed := time.Since(start) logger.Info(fmt.Sprintf("Generate finished (%s)", elapsed)) return } if marker == nil { logger.Errorf("nil marker, skipping generate") continue } wg.Add() task := GenerateMarkersTask{ TxnManager: s.TxnManager, Marker: marker, Overwrite: overwrite, fileNamingAlgorithm: fileNamingAlgo, } go task.Start(&wg) } wg.Wait() instance.Paths.Generated.EmptyTmpDir() elapsed := time.Since(start) logger.Info(fmt.Sprintf("Generate finished (%s)", elapsed)) }() } func (s *singleton) GenerateDefaultScreenshot(sceneId string) { s.generateScreenshot(sceneId, nil) } func (s *singleton) GenerateScreenshot(sceneId string, at float64) { s.generateScreenshot(sceneId, &at) } // generate default screenshot if at is nil func (s *singleton) generateScreenshot(sceneId string, at *float64) { if s.Status.Status != Idle { return } s.Status.SetStatus(Generate) s.Status.indefiniteProgress() instance.Paths.Generated.EnsureTmpDir() go func() { defer s.returnToIdleState() sceneIdInt, err := strconv.Atoi(sceneId) if err != nil { logger.Errorf("Error parsing scene id %s: %s", sceneId, err.Error()) return } var scene *models.Scene if err := s.TxnManager.WithReadTxn(context.TODO(), func(r models.ReaderRepository) error { var err error scene, err = r.Scene().Find(sceneIdInt) return err }); err != nil || scene == nil { logger.Errorf("failed to get scene for generate: %s", err.Error()) return } task := GenerateScreenshotTask{ txnManager: s.TxnManager, Scene: *scene, ScreenshotAt: at, fileNamingAlgorithm: config.GetInstance().GetVideoFileNamingAlgorithm(), } var wg sync.WaitGroup wg.Add(1) go task.Start(&wg) wg.Wait() logger.Infof("Generate screenshot finished") }() } func (s *singleton) isFileBasedAutoTag(input models.AutoTagMetadataInput) bool { const wildcard = "*" performerIds := input.Performers studioIds := input.Studios tagIds := input.Tags return (len(performerIds) == 0 || performerIds[0] == wildcard) && (len(studioIds) == 0 || studioIds[0] == wildcard) && (len(tagIds) == 0 || tagIds[0] == wildcard) } func (s *singleton) AutoTag(input models.AutoTagMetadataInput) { if s.Status.Status != Idle { return } s.Status.SetStatus(AutoTag) s.Status.indefiniteProgress() go func() { defer s.returnToIdleState() if s.isFileBasedAutoTag(input) { // doing file-based auto-tag s.autoTagScenes(input.Paths, len(input.Performers) > 0, len(input.Studios) > 0, len(input.Tags) > 0) } else { // doing specific performer/studio/tag auto-tag s.autoTagSpecific(input) } }() } func (s *singleton) autoTagScenes(paths []string, performers, studios, tags bool) { if err := s.TxnManager.WithReadTxn(context.TODO(), func(r models.ReaderRepository) error { ret := &models.SceneFilterType{} or := ret sep := string(filepath.Separator) for _, p := range paths { if !strings.HasSuffix(p, sep) { p = p + sep } if ret.Path == nil { or = ret } else { newOr := &models.SceneFilterType{} or.Or = newOr or = newOr } or.Path = &models.StringCriterionInput{ Modifier: models.CriterionModifierEquals, Value: p + "%", } } organized := false ret.Organized = &organized // batch process scenes batchSize := 1000 page := 1 findFilter := &models.FindFilterType{ PerPage: &batchSize, Page: &page, } more := true processed := 0 for more { scenes, total, err := r.Scene().Query(ret, findFilter) if err != nil { return err } if processed == 0 { logger.Infof("Starting autotag of %d scenes", total) } for _, ss := range scenes { if s.Status.stopping { logger.Info("Stopping due to user request") return nil } t := autoTagSceneTask{ txnManager: s.TxnManager, scene: ss, performers: performers, studios: studios, tags: tags, } var wg sync.WaitGroup wg.Add(1) go t.Start(&wg) wg.Wait() processed++ s.Status.setProgress(processed, total) } if len(scenes) != batchSize { more = false } else { page++ } } return nil }); err != nil { logger.Error(err.Error()) } logger.Info("Finished autotag") } func (s *singleton) autoTagSpecific(input models.AutoTagMetadataInput) { performerIds := input.Performers studioIds := input.Studios tagIds := input.Tags performerCount := len(performerIds) studioCount := len(studioIds) tagCount := len(tagIds) if err := s.TxnManager.WithReadTxn(context.TODO(), func(r models.ReaderRepository) error { performerQuery := r.Performer() studioQuery := r.Studio() tagQuery := r.Tag() const wildcard = "*" var err error if performerCount == 1 && performerIds[0] == wildcard { performerCount, err = performerQuery.Count() if err != nil { return fmt.Errorf("error getting performer count: %s", err.Error()) } } if studioCount == 1 && studioIds[0] == wildcard { studioCount, err = studioQuery.Count() if err != nil { return fmt.Errorf("error getting studio count: %s", err.Error()) } } if tagCount == 1 && tagIds[0] == wildcard { tagCount, err = tagQuery.Count() if err != nil { return fmt.Errorf("error getting tag count: %s", err.Error()) } } return nil }); err != nil { logger.Error(err.Error()) return } total := performerCount + studioCount + tagCount s.Status.setProgress(0, total) logger.Infof("Starting autotag of %d performers, %d studios, %d tags", performerCount, studioCount, tagCount) s.autoTagPerformers(input.Paths, performerIds) s.autoTagStudios(input.Paths, studioIds) s.autoTagTags(input.Paths, tagIds) logger.Info("Finished autotag") } func (s *singleton) autoTagPerformers(paths []string, performerIds []string) { if s.Status.stopping { return } for _, performerId := range performerIds { var performers []*models.Performer if err := s.TxnManager.WithReadTxn(context.TODO(), func(r models.ReaderRepository) error { performerQuery := r.Performer() if performerId == "*" { var err error performers, err = performerQuery.All() if err != nil { return fmt.Errorf("error querying performers: %s", err.Error()) } } else { performerIdInt, err := strconv.Atoi(performerId) if err != nil { return fmt.Errorf("error parsing performer id %s: %s", performerId, err.Error()) } performer, err := performerQuery.Find(performerIdInt) if err != nil { return fmt.Errorf("error finding performer id %s: %s", performerId, err.Error()) } if performer == nil { return fmt.Errorf("performer with id %s not found", performerId) } performers = append(performers, performer) } for _, performer := range performers { if s.Status.stopping { logger.Info("Stopping due to user request") return nil } if err := s.TxnManager.WithTxn(context.TODO(), func(r models.Repository) error { return autotag.PerformerScenes(performer, paths, r.Scene()) }); err != nil { return fmt.Errorf("error auto-tagging performer '%s': %s", performer.Name.String, err.Error()) } s.Status.incrementProgress() } return nil }); err != nil { logger.Error(err.Error()) continue } } } func (s *singleton) autoTagStudios(paths []string, studioIds []string) { if s.Status.stopping { return } for _, studioId := range studioIds { var studios []*models.Studio if err := s.TxnManager.WithReadTxn(context.TODO(), func(r models.ReaderRepository) error { studioQuery := r.Studio() if studioId == "*" { var err error studios, err = studioQuery.All() if err != nil { return fmt.Errorf("error querying studios: %s", err.Error()) } } else { studioIdInt, err := strconv.Atoi(studioId) if err != nil { return fmt.Errorf("error parsing studio id %s: %s", studioId, err.Error()) } studio, err := studioQuery.Find(studioIdInt) if err != nil { return fmt.Errorf("error finding studio id %s: %s", studioId, err.Error()) } if studio == nil { return fmt.Errorf("studio with id %s not found", studioId) } studios = append(studios, studio) } for _, studio := range studios { if s.Status.stopping { logger.Info("Stopping due to user request") return nil } if err := s.TxnManager.WithTxn(context.TODO(), func(r models.Repository) error { return autotag.StudioScenes(studio, paths, r.Scene()) }); err != nil { return fmt.Errorf("error auto-tagging studio '%s': %s", studio.Name.String, err.Error()) } s.Status.incrementProgress() } return nil }); err != nil { logger.Error(err.Error()) continue } } } func (s *singleton) autoTagTags(paths []string, tagIds []string) { if s.Status.stopping { return } for _, tagId := range tagIds { var tags []*models.Tag if err := s.TxnManager.WithReadTxn(context.TODO(), func(r models.ReaderRepository) error { tagQuery := r.Tag() if tagId == "*" { var err error tags, err = tagQuery.All() if err != nil { return fmt.Errorf("error querying tags: %s", err.Error()) } } else { tagIdInt, err := strconv.Atoi(tagId) if err != nil { return fmt.Errorf("error parsing tag id %s: %s", tagId, err.Error()) } tag, err := tagQuery.Find(tagIdInt) if err != nil { return fmt.Errorf("error finding tag id %s: %s", tagId, err.Error()) } tags = append(tags, tag) } for _, tag := range tags { if s.Status.stopping { logger.Info("Stopping due to user request") return nil } if err := s.TxnManager.WithTxn(context.TODO(), func(r models.Repository) error { return autotag.TagScenes(tag, paths, r.Scene()) }); err != nil { return fmt.Errorf("error auto-tagging tag '%s': %s", tag.Name, err.Error()) } s.Status.incrementProgress() } return nil }); err != nil { logger.Error(err.Error()) continue } } } func (s *singleton) Clean(input models.CleanMetadataInput) { if s.Status.Status != Idle { return } s.Status.SetStatus(Clean) s.Status.indefiniteProgress() go func() { defer s.returnToIdleState() var scenes []*models.Scene var images []*models.Image var galleries []*models.Gallery if err := s.TxnManager.WithReadTxn(context.TODO(), func(r models.ReaderRepository) error { qb := r.Scene() iqb := r.Image() gqb := r.Gallery() logger.Infof("Starting cleaning of tracked files") if input.DryRun { logger.Infof("Running in Dry Mode") } var err error scenes, err = qb.All() if err != nil { return errors.New("failed to fetch list of scenes for cleaning") } images, err = iqb.All() if err != nil { return errors.New("failed to fetch list of images for cleaning") } galleries, err = gqb.All() if err != nil { return errors.New("failed to fetch list of galleries for cleaning") } return nil }); err != nil { logger.Error(err.Error()) return } if s.Status.stopping { logger.Info("Stopping due to user request") return } var wg sync.WaitGroup s.Status.Progress = 0 total := len(scenes) + len(images) + len(galleries) fileNamingAlgo := config.GetInstance().GetVideoFileNamingAlgorithm() for i, scene := range scenes { s.Status.setProgress(i, total) if s.Status.stopping { logger.Info("Stopping due to user request") return } if scene == nil { logger.Errorf("nil scene, skipping Clean") continue } wg.Add(1) task := CleanTask{ TxnManager: s.TxnManager, Scene: scene, fileNamingAlgorithm: fileNamingAlgo, } go task.Start(&wg, input.DryRun) wg.Wait() } for i, img := range images { s.Status.setProgress(len(scenes)+i, total) if s.Status.stopping { logger.Info("Stopping due to user request") return } if img == nil { logger.Errorf("nil image, skipping Clean") continue } wg.Add(1) task := CleanTask{ TxnManager: s.TxnManager, Image: img, } go task.Start(&wg, input.DryRun) wg.Wait() } for i, gallery := range galleries { s.Status.setProgress(len(scenes)+len(galleries)+i, total) if s.Status.stopping { logger.Info("Stopping due to user request") return } if gallery == nil { logger.Errorf("nil gallery, skipping Clean") continue } wg.Add(1) task := CleanTask{ TxnManager: s.TxnManager, Gallery: gallery, } go task.Start(&wg, input.DryRun) wg.Wait() } logger.Info("Finished Cleaning") }() } func (s *singleton) MigrateHash() { if s.Status.Status != Idle { return } s.Status.SetStatus(Migrate) s.Status.indefiniteProgress() go func() { defer s.returnToIdleState() fileNamingAlgo := config.GetInstance().GetVideoFileNamingAlgorithm() logger.Infof("Migrating generated files for %s naming hash", fileNamingAlgo.String()) var scenes []*models.Scene if err := s.TxnManager.WithReadTxn(context.TODO(), func(r models.ReaderRepository) error { var err error scenes, err = r.Scene().All() return err }); err != nil { logger.Errorf("failed to fetch list of scenes for migration: %s", err.Error()) return } var wg sync.WaitGroup s.Status.Progress = 0 total := len(scenes) for i, scene := range scenes { s.Status.setProgress(i, total) if s.Status.stopping { logger.Info("Stopping due to user request") return } if scene == nil { logger.Errorf("nil scene, skipping migrate") continue } wg.Add(1) task := MigrateHashTask{Scene: scene, fileNamingAlgorithm: fileNamingAlgo} go task.Start(&wg) wg.Wait() } logger.Info("Finished migrating") }() } func (s *singleton) returnToIdleState() { if r := recover(); r != nil { logger.Info("recovered from ", r) } if s.Status.Status == Generate { instance.Paths.Generated.RemoveTmpDir() } s.Status.SetStatus(Idle) s.Status.indefiniteProgress() s.Status.stopping = false } type totalsGenerate struct { sprites int64 previews int64 imagePreviews int64 markers int64 transcodes int64 phashes int64 } func (s *singleton) neededGenerate(scenes []*models.Scene, input models.GenerateMetadataInput) *totalsGenerate { var totals totalsGenerate const timeout = 90 * time.Second // create a control channel through which to signal the counting loop when the timeout is reached chTimeout := make(chan struct{}) //run the timeout function in a separate thread go func() { time.Sleep(timeout) chTimeout <- struct{}{} }() fileNamingAlgo := config.GetInstance().GetVideoFileNamingAlgorithm() overwrite := false if input.Overwrite != nil { overwrite = *input.Overwrite } logger.Infof("Counting content to generate...") for _, scene := range scenes { if scene != nil { if input.Sprites { task := GenerateSpriteTask{ Scene: *scene, fileNamingAlgorithm: fileNamingAlgo, } if overwrite || task.required() { totals.sprites++ } } if input.Previews { task := GeneratePreviewTask{ Scene: *scene, ImagePreview: input.ImagePreviews, fileNamingAlgorithm: fileNamingAlgo, } sceneHash := scene.GetHash(task.fileNamingAlgorithm) if overwrite || !task.doesVideoPreviewExist(sceneHash) { totals.previews++ } if input.ImagePreviews && (overwrite || !task.doesImagePreviewExist(sceneHash)) { totals.imagePreviews++ } } if input.Markers { task := GenerateMarkersTask{ TxnManager: s.TxnManager, Scene: scene, Overwrite: overwrite, fileNamingAlgorithm: fileNamingAlgo, } totals.markers += int64(task.isMarkerNeeded()) } if input.Transcodes { task := GenerateTranscodeTask{ Scene: *scene, Overwrite: overwrite, fileNamingAlgorithm: fileNamingAlgo, } if task.isTranscodeNeeded() { totals.transcodes++ } } if input.Phashes { task := GeneratePhashTask{ Scene: *scene, fileNamingAlgorithm: fileNamingAlgo, } if task.shouldGenerate() { totals.phashes++ } } } //check for timeout select { case <-chTimeout: return nil default: } } return &totals }