2021-10-14 23:39:48 +00:00
|
|
|
package image
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2022-07-13 06:30:54 +00:00
|
|
|
"errors"
|
2021-10-14 23:39:48 +00:00
|
|
|
"fmt"
|
2022-07-13 06:30:54 +00:00
|
|
|
"path/filepath"
|
2021-10-14 23:39:48 +00:00
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/stashapp/stash/pkg/file"
|
|
|
|
"github.com/stashapp/stash/pkg/logger"
|
|
|
|
"github.com/stashapp/stash/pkg/models"
|
|
|
|
"github.com/stashapp/stash/pkg/plugin"
|
2022-07-13 06:30:54 +00:00
|
|
|
"github.com/stashapp/stash/pkg/sliceutil/intslice"
|
2021-10-14 23:39:48 +00:00
|
|
|
)
|
|
|
|
|
2022-07-13 06:30:54 +00:00
|
|
|
var (
|
|
|
|
ErrNotImageFile = errors.New("not an image file")
|
|
|
|
)
|
|
|
|
|
2022-05-19 07:49:32 +00:00
|
|
|
type FinderCreatorUpdater interface {
|
2022-07-13 06:30:54 +00:00
|
|
|
FindByFileID(ctx context.Context, fileID file.ID) ([]*models.Image, error)
|
|
|
|
FindByFingerprints(ctx context.Context, fp []file.Fingerprint) ([]*models.Image, error)
|
|
|
|
Create(ctx context.Context, newImage *models.ImageCreateInput) error
|
2022-08-12 02:21:46 +00:00
|
|
|
AddFileID(ctx context.Context, id int, fileID file.ID) error
|
|
|
|
models.GalleryIDLoader
|
2022-09-01 07:54:34 +00:00
|
|
|
models.ImageFileLoader
|
2022-05-19 07:49:32 +00:00
|
|
|
}
|
|
|
|
|
2022-07-13 06:30:54 +00:00
|
|
|
type GalleryFinderCreator interface {
|
|
|
|
FindByFileID(ctx context.Context, fileID file.ID) ([]*models.Gallery, error)
|
|
|
|
FindByFolderID(ctx context.Context, folderID file.FolderID) ([]*models.Gallery, error)
|
|
|
|
Create(ctx context.Context, newObject *models.Gallery, fileIDs []file.ID) error
|
2021-10-14 23:39:48 +00:00
|
|
|
}
|
|
|
|
|
2022-07-13 06:30:54 +00:00
|
|
|
type ScanConfig interface {
|
|
|
|
GetCreateGalleriesFromFolders() bool
|
|
|
|
IsGenerateThumbnails() bool
|
2021-10-14 23:39:48 +00:00
|
|
|
}
|
|
|
|
|
2022-07-13 06:30:54 +00:00
|
|
|
type ScanHandler struct {
|
|
|
|
CreatorUpdater FinderCreatorUpdater
|
|
|
|
GalleryFinder GalleryFinderCreator
|
2021-10-14 23:39:48 +00:00
|
|
|
|
2022-07-13 06:30:54 +00:00
|
|
|
ThumbnailGenerator ThumbnailGenerator
|
2021-10-14 23:39:48 +00:00
|
|
|
|
2022-07-13 06:30:54 +00:00
|
|
|
ScanConfig ScanConfig
|
2021-10-14 23:39:48 +00:00
|
|
|
|
2022-07-13 06:30:54 +00:00
|
|
|
PluginCache *plugin.Cache
|
|
|
|
}
|
2021-10-14 23:39:48 +00:00
|
|
|
|
2022-07-13 06:30:54 +00:00
|
|
|
func (h *ScanHandler) validate() error {
|
|
|
|
if h.CreatorUpdater == nil {
|
|
|
|
return errors.New("CreatorUpdater is required")
|
|
|
|
}
|
|
|
|
if h.GalleryFinder == nil {
|
|
|
|
return errors.New("GalleryFinder is required")
|
|
|
|
}
|
|
|
|
if h.ScanConfig == nil {
|
|
|
|
return errors.New("ScanConfig is required")
|
|
|
|
}
|
2021-10-14 23:39:48 +00:00
|
|
|
|
2022-07-13 06:30:54 +00:00
|
|
|
return nil
|
|
|
|
}
|
2021-10-14 23:39:48 +00:00
|
|
|
|
2022-07-13 06:30:54 +00:00
|
|
|
func (h *ScanHandler) Handle(ctx context.Context, f file.File) error {
|
|
|
|
if err := h.validate(); err != nil {
|
|
|
|
return err
|
2021-10-14 23:39:48 +00:00
|
|
|
}
|
|
|
|
|
2022-07-13 06:30:54 +00:00
|
|
|
imageFile, ok := f.(*file.ImageFile)
|
|
|
|
if !ok {
|
|
|
|
return ErrNotImageFile
|
|
|
|
}
|
2021-10-14 23:39:48 +00:00
|
|
|
|
2022-07-13 06:30:54 +00:00
|
|
|
// try to match the file to an image
|
|
|
|
existing, err := h.CreatorUpdater.FindByFileID(ctx, imageFile.ID)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("finding existing image: %w", err)
|
|
|
|
}
|
2021-10-14 23:39:48 +00:00
|
|
|
|
2022-07-13 06:30:54 +00:00
|
|
|
if len(existing) == 0 {
|
|
|
|
// try also to match file by fingerprints
|
|
|
|
existing, err = h.CreatorUpdater.FindByFingerprints(ctx, imageFile.Fingerprints)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("finding existing image by fingerprints: %w", err)
|
|
|
|
}
|
|
|
|
}
|
2021-10-14 23:39:48 +00:00
|
|
|
|
2022-07-13 06:30:54 +00:00
|
|
|
if len(existing) > 0 {
|
|
|
|
if err := h.associateExisting(ctx, existing, imageFile); err != nil {
|
2021-10-14 23:39:48 +00:00
|
|
|
return err
|
2022-07-13 06:30:54 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// create a new image
|
|
|
|
now := time.Now()
|
|
|
|
newImage := &models.Image{
|
2022-08-12 02:21:46 +00:00
|
|
|
CreatedAt: now,
|
|
|
|
UpdatedAt: now,
|
|
|
|
GalleryIDs: models.NewRelatedIDs([]int{}),
|
2021-10-14 23:39:48 +00:00
|
|
|
}
|
|
|
|
|
2022-07-13 06:30:54 +00:00
|
|
|
// if the file is in a zip, then associate it with the gallery
|
|
|
|
if imageFile.ZipFileID != nil {
|
|
|
|
g, err := h.GalleryFinder.FindByFileID(ctx, *imageFile.ZipFileID)
|
2021-10-14 23:39:48 +00:00
|
|
|
if err != nil {
|
2022-07-13 06:30:54 +00:00
|
|
|
return fmt.Errorf("finding gallery for zip file id %d: %w", *imageFile.ZipFileID, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, gg := range g {
|
2022-08-12 02:21:46 +00:00
|
|
|
newImage.GalleryIDs.Add(gg.ID)
|
2022-07-13 06:30:54 +00:00
|
|
|
}
|
|
|
|
} else if h.ScanConfig.GetCreateGalleriesFromFolders() {
|
|
|
|
if err := h.associateFolderBasedGallery(ctx, newImage, imageFile); err != nil {
|
|
|
|
return err
|
2021-10-14 23:39:48 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-18 00:51:59 +00:00
|
|
|
logger.Infof("%s doesn't exist. Creating new image...", f.Base().Path)
|
|
|
|
|
2022-07-13 06:30:54 +00:00
|
|
|
if err := h.CreatorUpdater.Create(ctx, &models.ImageCreateInput{
|
|
|
|
Image: newImage,
|
|
|
|
FileIDs: []file.ID{imageFile.ID},
|
|
|
|
}); err != nil {
|
|
|
|
return fmt.Errorf("creating new image: %w", err)
|
|
|
|
}
|
2021-10-14 23:39:48 +00:00
|
|
|
|
2022-09-19 04:53:06 +00:00
|
|
|
h.PluginCache.RegisterPostHooks(ctx, newImage.ID, plugin.ImageCreatePost, nil, nil)
|
2021-10-14 23:39:48 +00:00
|
|
|
|
2022-07-13 06:30:54 +00:00
|
|
|
existing = []*models.Image{newImage}
|
2021-10-14 23:39:48 +00:00
|
|
|
}
|
|
|
|
|
2022-07-13 06:30:54 +00:00
|
|
|
if h.ScanConfig.IsGenerateThumbnails() {
|
|
|
|
for _, s := range existing {
|
|
|
|
if err := h.ThumbnailGenerator.GenerateThumbnail(ctx, s, imageFile); err != nil {
|
|
|
|
// just log if cover generation fails. We can try again on rescan
|
|
|
|
logger.Errorf("Error generating thumbnail for %s: %v", imageFile.Path, err)
|
|
|
|
}
|
|
|
|
}
|
2021-10-14 23:39:48 +00:00
|
|
|
}
|
|
|
|
|
2022-07-13 06:30:54 +00:00
|
|
|
return nil
|
|
|
|
}
|
2021-10-14 23:39:48 +00:00
|
|
|
|
2022-07-13 06:30:54 +00:00
|
|
|
func (h *ScanHandler) associateExisting(ctx context.Context, existing []*models.Image, f *file.ImageFile) error {
|
|
|
|
for _, i := range existing {
|
2022-09-01 07:54:34 +00:00
|
|
|
if err := i.LoadFiles(ctx, h.CreatorUpdater); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2022-07-13 06:30:54 +00:00
|
|
|
found := false
|
2022-09-01 07:54:34 +00:00
|
|
|
for _, sf := range i.Files.List() {
|
2022-07-13 06:30:54 +00:00
|
|
|
if sf.ID == f.Base().ID {
|
|
|
|
found = true
|
|
|
|
break
|
2021-10-14 23:39:48 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-13 06:30:54 +00:00
|
|
|
if !found {
|
2022-09-02 01:18:37 +00:00
|
|
|
logger.Infof("Adding %s to image %s", f.Path, i.DisplayName())
|
2021-10-14 23:39:48 +00:00
|
|
|
|
2022-07-13 06:30:54 +00:00
|
|
|
// associate with folder-based gallery if applicable
|
|
|
|
if h.ScanConfig.GetCreateGalleriesFromFolders() {
|
|
|
|
if err := h.associateFolderBasedGallery(ctx, i, f); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2021-10-14 23:39:48 +00:00
|
|
|
}
|
|
|
|
|
2022-08-12 02:21:46 +00:00
|
|
|
if err := h.CreatorUpdater.AddFileID(ctx, i.ID, f.ID); err != nil {
|
|
|
|
return fmt.Errorf("adding file to image: %w", err)
|
2022-07-13 06:30:54 +00:00
|
|
|
}
|
2021-10-14 23:39:48 +00:00
|
|
|
}
|
2022-07-13 06:30:54 +00:00
|
|
|
}
|
2021-10-14 23:39:48 +00:00
|
|
|
|
2022-07-13 06:30:54 +00:00
|
|
|
return nil
|
|
|
|
}
|
2021-10-14 23:39:48 +00:00
|
|
|
|
2022-07-13 06:30:54 +00:00
|
|
|
func (h *ScanHandler) getOrCreateFolderBasedGallery(ctx context.Context, f file.File) (*models.Gallery, error) {
|
|
|
|
// don't create folder-based galleries for files in zip file
|
|
|
|
if f.Base().ZipFileID != nil {
|
|
|
|
return nil, nil
|
|
|
|
}
|
2021-10-14 23:39:48 +00:00
|
|
|
|
2022-07-13 06:30:54 +00:00
|
|
|
folderID := f.Base().ParentFolderID
|
|
|
|
g, err := h.GalleryFinder.FindByFolderID(ctx, folderID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("finding folder based gallery: %w", err)
|
2021-10-14 23:39:48 +00:00
|
|
|
}
|
|
|
|
|
2022-07-13 06:30:54 +00:00
|
|
|
if len(g) > 0 {
|
|
|
|
gg := g[0]
|
|
|
|
return gg, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// create a new folder-based gallery
|
|
|
|
now := time.Now()
|
|
|
|
newGallery := &models.Gallery{
|
|
|
|
FolderID: &folderID,
|
|
|
|
CreatedAt: now,
|
|
|
|
UpdatedAt: now,
|
|
|
|
}
|
|
|
|
|
|
|
|
logger.Infof("Creating folder-based gallery for %s", filepath.Dir(f.Base().Path))
|
|
|
|
if err := h.GalleryFinder.Create(ctx, newGallery, nil); err != nil {
|
|
|
|
return nil, fmt.Errorf("creating folder based gallery: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return newGallery, nil
|
2021-10-14 23:39:48 +00:00
|
|
|
}
|
2022-07-13 06:30:54 +00:00
|
|
|
|
|
|
|
func (h *ScanHandler) associateFolderBasedGallery(ctx context.Context, newImage *models.Image, f file.File) error {
|
|
|
|
g, err := h.getOrCreateFolderBasedGallery(ctx, f)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2022-08-12 02:21:46 +00:00
|
|
|
if err := newImage.LoadGalleryIDs(ctx, h.CreatorUpdater); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if g != nil && !intslice.IntInclude(newImage.GalleryIDs.List(), g.ID) {
|
|
|
|
newImage.GalleryIDs.Add(g.ID)
|
2022-09-01 07:54:34 +00:00
|
|
|
logger.Infof("Adding %s to folder-based gallery %s", f.Base().Path, g.Path)
|
2022-07-13 06:30:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|